diff --git a/README.md b/README.md
index e2657f23a..e91a29e7e 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c
 
 ## Latest News
 
+- 2020/09/19: Memory card importer/editor added to Qt frontend.
 - 2020/09/13: Support for chaining post processing shaders added.
 - 2020/09/12: Additional texture filtering options added.
 - 2020/09/09: Basic cheat support added. Not all instructions/commands are supported yet.
@@ -63,6 +64,7 @@ Other features include:
  - Automatic content scanning - game titles/regions are provided by redump.org
  - Optional automatic switching of memory cards for each game
  - Supports loading cheats from libretro or PCSXR format lists
+ - Memory card editor and save importer
 
 ## System Requirements
  - A CPU faster than a potato. But it needs to be 64-bit (either x86_64 or AArch64/ARMv8) otherwise you won't get a recompiler and it'll be slow. There are no plans to add any 32-bit recompilers.
diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt
index 776abe4de..38d9dfd1e 100644
--- a/src/duckstation-qt/CMakeLists.txt
+++ b/src/duckstation-qt/CMakeLists.txt
@@ -52,6 +52,9 @@ set(SRCS
   mainwindow.cpp
   mainwindow.h
   mainwindow.ui
+  memorycardeditordialog.cpp
+  memorycardeditordialog.h
+  memorycardeditordialog.ui
   memorycardsettingswidget.cpp
   memorycardsettingswidget.h
   postprocessingchainconfigwidget.cpp
diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj
index 3dc78fe92..cfdf8fd73 100644
--- a/src/duckstation-qt/duckstation-qt.vcxproj
+++ b/src/duckstation-qt/duckstation-qt.vcxproj
@@ -56,6 +56,7 @@
     
     
     
+    
     
     
     
@@ -70,6 +71,7 @@
     
     
     
+    
     
     
     
@@ -155,6 +157,9 @@
     
       Document
     
+    
+      Document
+    
   
   
     
@@ -181,6 +186,7 @@
     
     
     
+    
     
     
     
diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp
index eaee65150..5ec59f656 100644
--- a/src/duckstation-qt/mainwindow.cpp
+++ b/src/duckstation-qt/mainwindow.cpp
@@ -9,6 +9,7 @@
 #include "gamelistsettingswidget.h"
 #include "gamelistwidget.h"
 #include "gamepropertiesdialog.h"
+#include "memorycardeditordialog.h"
 #include "qtdisplaywidget.h"
 #include "qthostinterface.h"
 #include "qtutils.h"
@@ -670,6 +671,7 @@ void MainWindow::connectSignals()
   connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
   connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
   connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
+  connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
 
   connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError,
           Qt::BlockingQueuedConnection);
@@ -956,6 +958,15 @@ void MainWindow::onCheckForUpdatesActionTriggered()
   checkForUpdates(true);
 }
 
+void MainWindow::onToolsMemoryCardEditorTriggered()
+{
+  if (!m_memory_card_editor_dialog)
+    m_memory_card_editor_dialog = new MemoryCardEditorDialog(this);
+
+  m_memory_card_editor_dialog->setModal(false);
+  m_memory_card_editor_dialog->show();
+}
+
 void MainWindow::checkForUpdates(bool display_message)
 {
   if (!AutoUpdaterDialog::isSupported())
diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h
index e00ad67c9..c0bd4a3f6 100644
--- a/src/duckstation-qt/mainwindow.h
+++ b/src/duckstation-qt/mainwindow.h
@@ -14,6 +14,7 @@ class GameListWidget;
 class QtHostInterface;
 class QtDisplayWidget;
 class AutoUpdaterDialog;
+class MemoryCardEditorDialog;
 
 class HostDisplay;
 struct GameListEntry;
@@ -72,6 +73,7 @@ private Q_SLOTS:
   void onDiscordServerActionTriggered();
   void onAboutActionTriggered();
   void onCheckForUpdatesActionTriggered();
+  void onToolsMemoryCardEditorTriggered();
 
   void onGameListEntrySelected(const GameListEntry* entry);
   void onGameListEntryDoubleClicked(const GameListEntry* entry);
@@ -116,6 +118,7 @@ private:
 
   SettingsDialog* m_settings_dialog = nullptr;
   AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
+  MemoryCardEditorDialog* m_memory_card_editor_dialog = nullptr;
 
   bool m_emulation_running = false;
 };
diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui
index a229b8b73..2814523d8 100644
--- a/src/duckstation-qt/mainwindow.ui
+++ b/src/duckstation-qt/mainwindow.ui
@@ -14,7 +14,7 @@
    DuckStation
   
   
-   
+   
     :/icons/duck.png:/icons/duck.png
   
   
@@ -30,7 +30,7 @@
      0
      0
      754
-     30
+     21
     
    
    
@@ -78,7 +78,7 @@
       Save State
      
      
-      
+      
        :/icons/document-save.png:/icons/document-save.png
      
     
@@ -184,9 +184,16 @@
     
     
    
+   
    
    
    
+   
    
    
   
@@ -228,7 +235,7 @@
   
   
    
-    
+    
      :/icons/drive-optical.png:/icons/drive-optical.png
    
    
@@ -237,7 +244,7 @@
   
   
    
-    
+    
      :/icons/drive-removable-media.png:/icons/drive-removable-media.png
    
    
@@ -246,7 +253,7 @@
   
   
    
-    
+    
      :/icons/folder-open.png:/icons/folder-open.png
    
    
@@ -255,7 +262,7 @@
   
   
    
-    
+    
      :/icons/view-refresh.png:/icons/view-refresh.png
    
    
@@ -264,7 +271,7 @@
   
   
    
-    
+    
      :/icons/system-shutdown.png:/icons/system-shutdown.png
    
    
@@ -273,7 +280,7 @@
   
   
    
-    
+    
      :/icons/view-refresh.png:/icons/view-refresh.png
    
    
@@ -285,7 +292,7 @@
     true
    
    
-    
+    
      :/icons/media-playback-pause.png:/icons/media-playback-pause.png
    
    
@@ -294,7 +301,7 @@
   
   
    
-    
+    
      :/icons/document-open.png:/icons/document-open.png
    
    
@@ -303,7 +310,7 @@
   
   
    
-    
+    
      :/icons/document-save.png:/icons/document-save.png
    
    
@@ -317,7 +324,7 @@
   
   
    
-    
+    
      :/icons/utilities-system-monitor.png:/icons/utilities-system-monitor.png
    
    
@@ -326,7 +333,7 @@
   
   
    
-    
+    
      :/icons/input-gaming.png:/icons/input-gaming.png
    
    
@@ -335,7 +342,7 @@
   
   
    
-    
+    
      :/icons/applications-other.png:/icons/applications-other.png
    
    
@@ -344,7 +351,7 @@
   
   
    
-    
+    
      :/icons/video-display.png:/icons/video-display.png
    
    
@@ -353,7 +360,7 @@
   
   
    
-    
+    
      :/icons/antialias-icon.png:/icons/antialias-icon.png
    
    
@@ -362,7 +369,7 @@
   
   
    
-    
+    
      :/icons/applications-graphics.png:/icons/applications-graphics.png
    
    
@@ -371,7 +378,7 @@
   
   
    
-    
+    
      :/icons/view-fullscreen.png:/icons/view-fullscreen.png
    
    
@@ -410,7 +417,7 @@
   
   
    
-    
+    
      :/icons/media-optical.png:/icons/media-optical.png
    
    
@@ -419,7 +426,7 @@
   
   
    
-    
+    
      :/icons/conical-flask-red.png:/icons/conical-flask-red.png
    
    
@@ -428,7 +435,7 @@
   
   
    
-    
+    
      :/icons/audio-card.png:/icons/audio-card.png
    
    
@@ -437,7 +444,7 @@
   
   
    
-    
+    
      :/icons/folder-open.png:/icons/folder-open.png
    
    
@@ -446,7 +453,7 @@
   
   
    
-    
+    
      :/icons/applications-system.png:/icons/applications-system.png
    
    
@@ -455,7 +462,7 @@
   
   
    
-    
+    
      :/icons/applications-development.png:/icons/applications-development.png
    
    
@@ -464,7 +471,7 @@
   
   
    
-    
+    
      :/icons/edit-find.png:/icons/edit-find.png
    
    
@@ -473,7 +480,7 @@
   
   
    
-    
+    
      :/icons/preferences-system.png:/icons/preferences-system.png
    
    
@@ -584,7 +591,7 @@
   
   
    
-    
+    
      :/icons/camera-photo.png:/icons/camera-photo.png
    
    
@@ -593,7 +600,7 @@
   
   
    
-    
+    
      :/icons/media-flash-24.png:/icons/media-flash-24.png
    
    
@@ -602,7 +609,7 @@
   
   
    
-    
+    
      :/icons/media-playback-start.png:/icons/media-playback-start.png
    
    
@@ -647,6 +654,11 @@
     System &Display
    
   
+  
+   
+    Memory &Card Editor
+   
+  
  
  
   
diff --git a/src/duckstation-qt/memorycardeditordialog.cpp b/src/duckstation-qt/memorycardeditordialog.cpp
new file mode 100644
index 000000000..afc00717f
--- /dev/null
+++ b/src/duckstation-qt/memorycardeditordialog.cpp
@@ -0,0 +1,389 @@
+#include "memorycardeditordialog.h"
+#include "common/file_system.h"
+#include "common/string_util.h"
+#include "core/host_interface.h"
+#include "qtutils.h"
+#include 
+#include 
+#include 
+
+static constexpr char MEMORY_CARD_IMAGE_FILTER[] =
+  QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Memory Card Types (*.mcd *.mcr *.mc)");
+static constexpr char MEMORY_CARD_IMPORT_FILTER[] =
+  QT_TRANSLATE_NOOP("MemoryCardEditorDialog", "All Importable Memory Card Types (*.mcd *.mcr *.mc *.gme)");
+
+MemoryCardEditorDialog::MemoryCardEditorDialog(QWidget* parent) : QDialog(parent)
+{
+  m_ui.setupUi(this);
+  m_card_a.path_cb = m_ui.cardAPath;
+  m_card_a.table = m_ui.cardA;
+  m_card_a.blocks_free_label = m_ui.cardAUsage;
+  m_card_a.save_button = m_ui.saveCardA;
+  m_card_b.path_cb = m_ui.cardBPath;
+  m_card_b.table = m_ui.cardB;
+  m_card_b.blocks_free_label = m_ui.cardBUsage;
+  m_card_b.save_button = m_ui.saveCardB;
+
+  connectUi();
+  populateComboBox(m_ui.cardAPath);
+  populateComboBox(m_ui.cardBPath);
+}
+
+MemoryCardEditorDialog::~MemoryCardEditorDialog() = default;
+
+void MemoryCardEditorDialog::resizeEvent(QResizeEvent* ev)
+{
+  QtUtils::ResizeColumnsForTableView(m_card_a.table, {32, -1, 100, 45});
+  QtUtils::ResizeColumnsForTableView(m_card_b.table, {32, -1, 100, 45});
+}
+
+void MemoryCardEditorDialog::closeEvent(QCloseEvent* ev)
+{
+  promptForSave(&m_card_a);
+  promptForSave(&m_card_b);
+}
+
+void MemoryCardEditorDialog::connectUi()
+{
+  connect(m_ui.cardA, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardASelectionChanged);
+  connect(m_ui.cardB, &QTableWidget::itemSelectionChanged, this, &MemoryCardEditorDialog::onCardBSelectionChanged);
+  connect(m_ui.moveLeft, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
+  connect(m_ui.moveRight, &QPushButton::clicked, this, &MemoryCardEditorDialog::doCopyFile);
+  connect(m_ui.deleteFile, &QPushButton::clicked, this, &MemoryCardEditorDialog::doDeleteFile);
+
+  connect(m_ui.cardAPath, QOverload::of(&QComboBox::currentIndexChanged),
+          [this](int index) { loadCardFromComboBox(&m_card_a, index); });
+  connect(m_ui.cardBPath, QOverload::of(&QComboBox::currentIndexChanged),
+          [this](int index) { loadCardFromComboBox(&m_card_b, index); });
+  connect(m_ui.newCardA, &QPushButton::clicked, [this]() { newCard(&m_card_a); });
+  connect(m_ui.newCardB, &QPushButton::clicked, [this]() { newCard(&m_card_b); });
+  connect(m_ui.saveCardA, &QPushButton::clicked, [this]() { saveCard(&m_card_a); });
+  connect(m_ui.saveCardB, &QPushButton::clicked, [this]() { saveCard(&m_card_b); });
+  connect(m_ui.importCardA, &QPushButton::clicked, [this]() { importCard(&m_card_a); });
+  connect(m_ui.importCardB, &QPushButton::clicked, [this]() { importCard(&m_card_b); });
+}
+
+void MemoryCardEditorDialog::populateComboBox(QComboBox* cb)
+{
+  QSignalBlocker sb(cb);
+
+  cb->clear();
+
+  cb->addItem(QString());
+  cb->addItem(tr("Browse..."));
+
+  const std::string base_path(g_host_interface->GetUserDirectoryRelativePath("memcards"));
+  FileSystem::FindResultsArray results;
+  FileSystem::FindFiles(base_path.c_str(), "*.mcd", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_RELATIVE_PATHS, &results);
+  for (FILESYSTEM_FIND_DATA& fd : results)
+  {
+    std::string real_filename(
+      StringUtil::StdStringFromFormat("%s%c%s", base_path.c_str(), FS_OSPATH_SEPERATOR_CHARACTER, fd.FileName.c_str()));
+    std::string::size_type pos = fd.FileName.rfind('.');
+    if (pos != std::string::npos)
+      fd.FileName.erase(pos);
+
+    cb->addItem(QString::fromStdString(fd.FileName), QVariant(QString::fromStdString(real_filename)));
+  }
+}
+
+void MemoryCardEditorDialog::loadCardFromComboBox(Card* card, int index)
+{
+  QString filename;
+  if (index == 1)
+  {
+    filename = QFileDialog::getOpenFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER));
+    if (!filename.isEmpty())
+    {
+      // add to combo box
+      QFileInfo file(filename);
+      QSignalBlocker sb(card->path_cb);
+      card->path_cb->addItem(file.baseName(), QVariant(filename));
+      card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
+    }
+  }
+  else
+  {
+    filename = card->path_cb->itemData(index).toString();
+  }
+
+  if (filename.isEmpty())
+    return;
+
+  loadCard(filename, card);
+}
+
+void MemoryCardEditorDialog::onCardASelectionChanged()
+{
+  {
+    QSignalBlocker cb(m_card_b.table);
+    m_card_b.table->clearSelection();
+  }
+
+  updateButtonState();
+}
+
+void MemoryCardEditorDialog::onCardBSelectionChanged()
+{
+  {
+    QSignalBlocker cb(m_card_a.table);
+    m_card_a.table->clearSelection();
+  }
+
+  updateButtonState();
+}
+
+void MemoryCardEditorDialog::clearSelection()
+{
+  {
+    QSignalBlocker cb(m_card_a.table);
+    m_card_a.table->clearSelection();
+  }
+
+  {
+    QSignalBlocker cb(m_card_b.table);
+    m_card_b.table->clearSelection();
+  }
+
+  updateButtonState();
+}
+
+bool MemoryCardEditorDialog::loadCard(const QString& filename, Card* card)
+{
+  promptForSave(card);
+
+  card->table->setRowCount(0);
+  card->dirty = false;
+  card->blocks_free_label->clear();
+  card->save_button->setEnabled(false);
+
+  card->filename.clear();
+
+  std::string filename_str = filename.toStdString();
+  if (!MemoryCardImage::LoadFromFile(&card->data, filename_str.c_str()))
+  {
+    QMessageBox::critical(this, tr("Error"), tr("Failed to load memory card image."));
+    return false;
+  }
+
+  card->filename = std::move(filename_str);
+  updateCardTable(card);
+  updateCardBlocksFree(card);
+  updateButtonState();
+  return true;
+}
+
+void MemoryCardEditorDialog::updateCardTable(Card* card)
+{
+  card->table->setRowCount(0);
+
+  card->files = MemoryCardImage::EnumerateFiles(card->data);
+  for (const MemoryCardImage::FileInfo& fi : card->files)
+  {
+    const int row = card->table->rowCount();
+    card->table->insertRow(row);
+
+    if (!fi.icon_frames.empty())
+    {
+      const QImage image(reinterpret_cast(fi.icon_frames[0].pixels), MemoryCardImage::ICON_WIDTH,
+                         MemoryCardImage::ICON_HEIGHT, QImage::Format_RGBA8888);
+
+      QTableWidgetItem* icon = new QTableWidgetItem();
+      icon->setIcon(QIcon(QPixmap::fromImage(image)));
+      card->table->setItem(row, 0, icon);
+    }
+
+    card->table->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(fi.title)));
+    card->table->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(fi.filename)));
+    card->table->setItem(row, 3, new QTableWidgetItem(QStringLiteral("%1").arg(fi.num_blocks)));
+  }
+}
+
+void MemoryCardEditorDialog::updateCardBlocksFree(Card* card)
+{
+  card->blocks_free = MemoryCardImage::GetFreeBlockCount(card->data);
+  card->blocks_free_label->setText(
+    tr("%1 blocks free%2").arg(card->blocks_free).arg(card->dirty ? QStringLiteral(" (*)") : QString()));
+}
+
+void MemoryCardEditorDialog::setCardDirty(Card* card)
+{
+  card->dirty = true;
+  card->save_button->setEnabled(true);
+}
+
+void MemoryCardEditorDialog::newCard(Card* card)
+{
+  promptForSave(card);
+
+  QString filename =
+    QFileDialog::getSaveFileName(this, tr("Select Memory Card"), QString(), tr(MEMORY_CARD_IMAGE_FILTER));
+  if (filename.isEmpty())
+    return;
+
+  {
+    // add to combo box
+    QFileInfo file(filename);
+    QSignalBlocker sb(card->path_cb);
+    card->path_cb->addItem(file.baseName(), QVariant(filename));
+    card->path_cb->setCurrentIndex(card->path_cb->count() - 1);
+  }
+
+  card->filename = filename.toStdString();
+
+  MemoryCardImage::Format(&card->data);
+  updateCardTable(card);
+  updateCardBlocksFree(card);
+  updateButtonState();
+  saveCard(card);
+}
+
+void MemoryCardEditorDialog::saveCard(Card* card)
+{
+  if (card->filename.empty())
+    return;
+
+  if (!MemoryCardImage::SaveToFile(card->data, card->filename.c_str()))
+  {
+    QMessageBox::critical(this, tr("Error"),
+                          tr("Failed to write card to '%1'").arg(QString::fromStdString(card->filename)));
+    return;
+  }
+
+  card->dirty = false;
+  card->save_button->setEnabled(false);
+  updateCardBlocksFree(card);
+}
+
+void MemoryCardEditorDialog::promptForSave(Card* card)
+{
+  if (card->filename.empty() || !card->dirty)
+    return;
+
+  if (QMessageBox::question(this, tr("Save memory card?"),
+                            tr("Memory card '%1' is not saved, do you want to save before closing?")
+                              .arg(QString::fromStdString(card->filename)),
+                            QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)
+  {
+    return;
+  }
+
+  saveCard(card);
+}
+
+void MemoryCardEditorDialog::doCopyFile()
+{
+  const auto [src, fi] = getSelectedFile();
+  if (!fi)
+    return;
+
+  Card* dst = (src == &m_card_a) ? &m_card_b : &m_card_a;
+
+  if (dst->blocks_free < fi->num_blocks)
+  {
+    QMessageBox::critical(this, tr("Error"),
+                          tr("Insufficient blocks, this file needs %1 but only %2 are available.")
+                            .arg(fi->num_blocks)
+                            .arg(dst->blocks_free));
+    return;
+  }
+
+  std::vector buffer;
+  if (!MemoryCardImage::ReadFile(src->data, *fi, &buffer))
+  {
+    QMessageBox::critical(this, tr("Error"), tr("Failed to read file %1").arg(QString::fromStdString(fi->filename)));
+    return;
+  }
+
+  if (!MemoryCardImage::WriteFile(&dst->data, fi->filename, buffer))
+  {
+    QMessageBox::critical(this, tr("Error"), tr("Failed to write file %1").arg(QString::fromStdString(fi->filename)));
+    return;
+  }
+
+  clearSelection();
+  updateCardTable(dst);
+  updateCardBlocksFree(dst);
+  setCardDirty(dst);
+  updateButtonState();
+}
+
+void MemoryCardEditorDialog::doDeleteFile()
+{
+  const auto [card, fi] = getSelectedFile();
+  if (!fi)
+    return;
+
+  if (!MemoryCardImage::DeleteFile(&card->data, *fi))
+  {
+    QMessageBox::critical(this, tr("Error"), tr("Failed to delete file %1").arg(QString::fromStdString(fi->filename)));
+    return;
+  }
+
+  clearSelection();
+  updateCardTable(card);
+  updateCardBlocksFree(card);
+  setCardDirty(card);
+  updateButtonState();
+}
+
+void MemoryCardEditorDialog::importCard(Card* card)
+{
+  promptForSave(card);
+
+  QString filename =
+    QFileDialog::getOpenFileName(this, tr("Select Import File"), QString(), tr(MEMORY_CARD_IMPORT_FILTER));
+  if (filename.isEmpty())
+    return;
+
+  std::unique_ptr temp = std::make_unique();
+  if (!MemoryCardImage::ImportCard(temp.get(), filename.toStdString().c_str()))
+  {
+    QMessageBox::critical(this, tr("Error"), tr("Failed to import memory card. The log may contain more information."));
+    return;
+  }
+
+  clearSelection();
+
+  card->data = *temp;
+  updateCardTable(card);
+  updateCardBlocksFree(card);
+  setCardDirty(card);
+  updateButtonState();
+}
+
+std::tuple MemoryCardEditorDialog::getSelectedFile()
+{
+  QList sel = m_card_a.table->selectedRanges();
+  Card* card = &m_card_a;
+
+  if (sel.isEmpty())
+  {
+    sel = m_card_b.table->selectedRanges();
+    card = &m_card_b;
+  }
+
+  if (sel.isEmpty())
+    return std::tuple(nullptr, nullptr);
+
+  const int index = sel.front().topRow();
+  Assert(index >= 0 && static_cast(index) < card->files.size());
+
+  return std::tuple(card, &card->files[index]);
+}
+
+void MemoryCardEditorDialog::updateButtonState()
+{
+  const auto [selected_card, selected_file] = getSelectedFile();
+  const bool is_card_b = (selected_card == &m_card_b);
+  const bool has_selection = (selected_file != nullptr);
+  const bool card_a_present = !m_card_a.filename.empty();
+  const bool card_b_present = !m_card_b.filename.empty();
+  const bool both_cards_present = card_a_present && card_b_present;
+  m_ui.deleteFile->setEnabled(has_selection);
+  m_ui.exportFile->setEnabled(has_selection);
+  m_ui.moveLeft->setEnabled(both_cards_present && has_selection && is_card_b);
+  m_ui.moveRight->setEnabled(both_cards_present && has_selection && !is_card_b);
+  m_ui.importCardA->setEnabled(card_a_present);
+  m_ui.importCardB->setEnabled(card_b_present);
+}
diff --git a/src/duckstation-qt/memorycardeditordialog.h b/src/duckstation-qt/memorycardeditordialog.h
new file mode 100644
index 000000000..58ef95691
--- /dev/null
+++ b/src/duckstation-qt/memorycardeditordialog.h
@@ -0,0 +1,63 @@
+#pragma once
+#include "core/memory_card_image.h"
+#include "ui_memorycardeditordialog.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+class MemoryCardEditorDialog : public QDialog
+{
+  Q_OBJECT
+
+public:
+  MemoryCardEditorDialog(QWidget* parent);
+  ~MemoryCardEditorDialog();
+
+protected:
+  void resizeEvent(QResizeEvent* ev);
+  void closeEvent(QCloseEvent* ev);
+
+private Q_SLOTS:
+  void onCardASelectionChanged();
+  void onCardBSelectionChanged();
+  void doCopyFile();
+  void doDeleteFile();
+
+private:
+  struct Card
+  {
+    std::string filename;
+    MemoryCardImage::DataArray data;
+    std::vector files;
+    u32 blocks_free = 0;
+    bool dirty = false;
+
+    QComboBox* path_cb = nullptr;
+    QTableWidget* table = nullptr;
+    QLabel* blocks_free_label = nullptr;
+    QPushButton* save_button = nullptr;
+  };
+
+  void connectUi();
+  void populateComboBox(QComboBox* cb);
+  void clearSelection();
+  void loadCardFromComboBox(Card* card, int index);
+  bool loadCard(const QString& filename, Card* card);
+  void updateCardTable(Card* card);
+  void updateCardBlocksFree(Card* card);
+  void setCardDirty(Card* card);
+  void newCard(Card* card);
+  void saveCard(Card* card);
+  void promptForSave(Card* card);
+  void importCard(Card* card);
+
+  std::tuple getSelectedFile();
+  void updateButtonState();
+
+  Ui::MemoryCardEditorDialog m_ui;
+
+  Card m_card_a;
+  Card m_card_b;
+};
diff --git a/src/duckstation-qt/memorycardeditordialog.ui b/src/duckstation-qt/memorycardeditordialog.ui
new file mode 100644
index 000000000..d09e2e945
--- /dev/null
+++ b/src/duckstation-qt/memorycardeditordialog.ui
@@ -0,0 +1,290 @@
+
+
+ MemoryCardEditorDialog
+ 
+  
+   
+    0
+    0
+    846
+    515
+   
+  
+  
+   Memory Card Editor
+  
+  
+   - 
+    
+     
+      QAbstractItemView::SingleSelection
+     
+     
+      QAbstractItemView::SelectRows
+     
+     
+      
+       16
+       16
+      
+     
+     
+      true
+     
+     
+      false
+     
+     
+      
+       
+      
+     
+     
+      
+       Title
+      
+     
+     
+      
+       File Name
+      
+     
+     
+      
+       Blocks
+      
+     
+    
+   
 
+   - 
+    
+     
- 
+      
+       
+        Memory Card:
+       
+      
+     
 
+     - 
+      
+       
+        
+       
+      
+     
 
+     - 
+      
+       
+        New...
+       
+      
+     
 
+    
+    
+   - 
+    
+     
- 
+      
+       
+        0 blocks used
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Import File...
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Import Card...
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Save
+       
+      
+     
 
+    
+    
+   - 
+    
+     
- 
+      
+       
+        0 blocks used
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Import File...
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Import Card...
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Save
+       
+      
+     
 
+    
+    
+   - 
+    
+     
- 
+      
+       
+        Memory Card:
+       
+      
+     
 
+     - 
+      
+     
 
+     - 
+      
+       
+        New...
+       
+      
+     
 
+    
+    
+   - 
+    
+     
+      QAbstractItemView::SingleSelection
+     
+     
+      QAbstractItemView::SelectRows
+     
+     
+      
+       16
+       16
+      
+     
+     
+      true
+     
+     
+      false
+     
+     
+      
+       
+      
+     
+     
+      
+       Title
+      
+     
+     
+      
+       File Name
+      
+     
+     
+      
+       Blocks
+      
+     
+    
+   
 
+   - 
+    
+     
- 
+      
+       
+        false
+       
+       
+        Delete File
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        Export File
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        <<
+       
+      
+     
 
+     - 
+      
+       
+        false
+       
+       
+        >>
+       
+      
+     
 
+     - 
+      
+       
+        Qt::Vertical
+       
+       
+        
+         20
+         40
+        
+       
+      
+     
 
+    
+    
+  
+ 
+ 
+ 
+