diff --git a/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeRollbackBatch.php b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeRollbackBatch.php
new file mode 100644
index 0000000..2ca582b
--- /dev/null
+++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeRollbackBatch.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Drupal\migrate_drupal_ui\Batch;
+
+use Drupal\Core\Link;
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\MigrateExecutable;
+
+/**
+ * Runs a single migration batch.
+ */
+class MigrateUpgradeRollbackBatch {
+
+  /**
+   * Maximum number of previous messages to display.
+   */
+  const MESSAGE_LENGTH = 20;
+
+  /**
+   * The processed items for one batch of a given migration.
+   *
+   * @var int
+   */
+  protected static $numProcessed = 0;
+
+  /**
+   * MigrateMessage instance to capture messages during the migration process.
+   *
+   * @var \Drupal\migrate_drupal_ui\Batch\MigrateMessageCapture
+   */
+  protected static $messages;
+
+  /**
+   * Runs a single rollback batch.
+   *
+   * @param int[] $initial_ids
+   *   The full set of migration IDs to import.
+   * @param array $config
+   *   An array of additional configuration from the form.
+   * @param array $context
+   *   The batch context.
+   */
+  public static function run(array $initial_ids, array $config, array &$context) {
+    if (!isset($context['sandbox']['migration_ids'])) {
+      $context['sandbox']['max'] = count($initial_ids);
+      $context['sandbox']['current'] = 1;
+      // Total number processed for this migration.
+      $context['sandbox']['num_processed'] = 0;
+      // migration_ids will be the list of IDs remaining to run.
+      $context['sandbox']['migration_ids'] = $initial_ids;
+      $context['sandbox']['messages'] = [];
+      $context['results']['failures'] = 0;
+      $context['results']['successes'] = 0;
+    }
+
+    // Number processed in this batch.
+    static::$numProcessed = 0;
+
+    $migration_id = reset($context['sandbox']['migration_ids']);
+    $definition = \Drupal::service('plugin.manager.migration')->getDefinition($migration_id);
+    $configuration = [];
+
+    // @todo Find a way to avoid this in https://www.drupal.org/node/2804611.
+    if ($definition['destination']['plugin'] === 'entity:file') {
+      // Make sure we have a single trailing slash.
+      if ($definition['source']['plugin'] === 'd7_file_private') {
+        $configuration['source']['constants']['source_base_path'] = rtrim($config['source_private_file_path'], '/') . '/';
+      }
+      $configuration['source']['constants']['source_base_path'] = rtrim($config['source_base_path'], '/') . '/';
+    }
+
+    /** @var \Drupal\migrate\Plugin\Migration $migration */
+    $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id, $configuration);
+
+    if ($migration) {
+      static::$messages = new MigrateMessageCapture();
+      $executable = new MigrateExecutable($migration, static::$messages);
+
+      $migration_name = $migration->label() ? $migration->label() : $migration_id;
+
+      try {
+        $migration_status = $executable->rollback();
+      }
+      catch (\Exception $e) {
+        \Drupal::logger('migrate_drupal_ui')->error($e->getMessage());
+        $migration_status = MigrationInterface::RESULT_FAILED;
+      }
+
+      switch ($migration_status) {
+        case MigrationInterface::RESULT_COMPLETED:
+          // Store the number processed in the sandbox.
+          $context['sandbox']['num_processed'] += static::$numProcessed;
+          $message = new PluralTranslatableMarkup(
+            $context['sandbox']['num_processed'], 'Rolled back @migration (processed 1 item total)', 'Rolled back @migration (processed @count items total)',
+            ['@migration' => $migration_name]);
+          $context['sandbox']['messages'][] = $message;
+          \Drupal::logger('migrate_drupal_ui')->notice($message);
+          $context['sandbox']['num_processed'] = 0;
+          $context['results']['successes']++;
+          break;
+
+        case MigrationInterface::RESULT_INCOMPLETE:
+          $context['sandbox']['messages'][] = (string) new PluralTranslatableMarkup(
+            static::$numProcessed, 'Continuing with @migration (processed 1 item)', 'Continuing with @migration (processed @count items)',
+            ['@migration' => $migration_name]);
+          $context['sandbox']['num_processed'] += static::$numProcessed;
+          break;
+
+        case MigrationInterface::RESULT_STOPPED:
+          $context['sandbox']['messages'][] = t('Operation stopped by request');
+          break;
+
+        case MigrationInterface::RESULT_FAILED:
+          $context['sandbox']['messages'][] = t('Operation on @migration failed', ['@migration' => $migration_name]);
+          $context['results']['failures']++;
+          static::logger()->error('Operation on @migration failed', ['@migration' => $migration_name]);
+          break;
+
+        case MigrationInterface::RESULT_SKIPPED:
+          $context['sandbox']['messages'][] = (string) new TranslatableMarkup('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
+          \Drupal::logger('migrate_drupal_ui')->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]);
+          break;
+
+        case MigrationInterface::RESULT_DISABLED:
+          // Skip silently if disabled.
+          break;
+      }
+
+      // Unless we're continuing on with this migration, take it off the list.
+      if ($migration_status != MigrationInterface::RESULT_INCOMPLETE) {
+        array_shift($context['sandbox']['migration_ids']);
+        $context['sandbox']['current']++;
+      }
+
+      // Add and log any captured messages.
+      foreach (static::$messages->getMessages() as $message) {
+        $context['sandbox']['messages'][] = $message;
+        \Drupal::logger('migrate_drupal_ui')->error($message);
+      }
+
+      // Only display the last MESSAGE_LENGTH messages, in reverse order.
+      $message_count = count($context['sandbox']['messages']);
+      $context['message'] = '';
+      for ($index = max(0, $message_count - self::MESSAGE_LENGTH); $index < $message_count; $index++) {
+        $context['message'] = $context['sandbox']['messages'][$index] . "<br />\n" . $context['message'];
+      }
+      if ($message_count > self::MESSAGE_LENGTH) {
+        // Indicate there are earlier messages not displayed.
+        $context['message'] .= '&hellip;';
+      }
+      // At the top of the list, display the next one (which will be the one
+      // that is running while this message is visible).
+      if (!empty($context['sandbox']['migration_ids'])) {
+        $migration_id = reset($context['sandbox']['migration_ids']);
+        $migration = \Drupal::service('plugin.manager.migration')->createInstance($migration_id);
+        $migration_name = $migration->label() ? $migration->label() : $migration_id;
+        $context['message'] = (string) new TranslatableMarkup('Currently rolling back @migration (@current of @max total tasks)', [
+          '@migration' => $migration_name,
+          '@current' => $context['sandbox']['current'],
+          '@max' => $context['sandbox']['max'],
+        ]) . "<br />\n" . $context['message'];
+      }
+    }
+    else {
+      array_shift($context['sandbox']['migration_ids']);
+      $context['sandbox']['current']++;
+    }
+
+    $context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max'];
+  }
+
+  /**
+   * Callback executed when Migrate Upgrade Rollback batch process completes.
+   *
+   * @param bool $success
+   *   TRUE if batch successfully completed.
+   * @param array $results
+   *   Batch results.
+   * @param array $operations
+   *   An array of methods run in the batch.
+   * @param string $elapsed
+   *   The time to run the batch.
+   */
+  public static function finished($success, array $results, array $operations, $elapsed) {
+    $successes = $results['successes'];
+    $failures = $results['failures'];
+
+    // If we had any successes log that for the user.
+    if ($successes > 0) {
+      drupal_set_message(\Drupal::translation()->formatPlural($successes, 'Completed 1 rollback task successfully', 'Completed @count rollback tasks successfully'));
+    }
+
+    // If we had failures, log them and show the migration failed.
+    if ($failures > 0) {
+      drupal_set_message(\Drupal::translation()->formatPlural($failures, '1 rollback failed', '@count rollbacks failed'));
+      drupal_set_message(t('Rollback process not completed'), 'error');
+    }
+    else {
+      drupal_set_message(t('Rollback of the upgrade is complete - you may now start the upgrade process from scratch.'));
+    }
+
+    if (\Drupal::moduleHandler()->moduleExists('dblog')) {
+      $url = Url::fromRoute('migrate_drupal_ui.log');
+      drupal_set_message(Link::fromTextAndUrl(new TranslatableMarkup('Review the detailed upgrade log'), $url), $failures ? 'error' : 'status');
+    }
+  }
+
+}
diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
index 04da0f5..2ece404 100644
--- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
+++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
@@ -14,6 +14,7 @@
 use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
 use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManagerInterface;
 use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch;
+use Drupal\migrate_drupal_ui\Batch\MigrateUpgradeRollbackBatch;
 use Drupal\migrate_drupal\MigrationConfigurationTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -27,6 +28,16 @@ class MigrateUpgradeForm extends ConfirmFormBase {
   use MigrationConfigurationTrait;
 
   /**
+   * If a migration has previously run, perform an incremental migration.
+   */
+  const MIGRATE_UPGRADE_INCREMENTAL = 1;
+
+  /**
+   * If a migration has previously run, roll it back and start fresh.
+   */
+  const MIGRATE_UPGRADE_ROLLBACK = 2;
+
+  /**
    * The state service.
    *
    * @var \Drupal\Core\State\StateInterface
@@ -262,10 +273,18 @@ public function buildOverviewForm(array $form, FormStateInterface $form_state) {
       //   https://www.drupal.org/node/2687849
       $form['upgrade_option_item'] = [
         '#type' => 'item',
-        '#prefix' => $this->t('An upgrade has already been performed on this site. To perform a new migration, create a clean and empty new install of Drupal 8. Rollbacks and incremental migrations are not yet supported through the user interface. For more information, see the <a href=":url">upgrading handbook</a>.', [':url' => 'https://www.drupal.org/upgrade/migrate']),
+        '#prefix' => $this->t('An upgrade has already been performed on this site.'),
         '#description' => $this->t('Last upgrade: @date', ['@date' => $this->dateFormatter->format($date_performed)]),
       ];
-      return $form;
+      $form['upgrade_option'] = array(
+        '#type' => 'radios',
+        '#title' => $this->t('You can rollback the upgrade.'),
+        '#default_value' => static::MIGRATE_UPGRADE_ROLLBACK,
+        '#options' => [
+          static::MIGRATE_UPGRADE_ROLLBACK => $this->t('<strong>Rollback</strong>: Remove content and configuration entities (such as fields and node types). Default values of other configuration will not be reverted (such as site name).'),
+        ],
+      );
+      $validate = ['::validateCredentialForm'];
     }
     else {
       $form['info_header'] = [
@@ -328,6 +347,15 @@ public function buildOverviewForm(array $form, FormStateInterface $form_state) {
    */
   public function submitOverviewForm(array &$form, FormStateInterface $form_state) {
     $form_state->set('step', 'credentials');
+    switch ($form_state->getValue('upgrade_option')) {
+      case static::MIGRATE_UPGRADE_ROLLBACK:
+        $form_state->setValue('step', 'confirm');
+        break;
+
+      default:
+        $form_state->setValue('step', 'credentials');
+        break;
+    }
     $form_state->setRebuild();
   }
 
@@ -476,6 +504,10 @@ public function buildCredentialForm(array $form, FormStateInterface $form_state)
    *   The current state of the form.
    */
   public function validateCredentialForm(array &$form, FormStateInterface $form_state) {
+    // Skip if rollback was chosen.
+    if ($form_state->getValue('upgrade_option') == static::MIGRATE_UPGRADE_ROLLBACK) {
+      return;
+    }
 
     // Retrieve the database driver from the form, use reflection to get the
     // namespace, and then construct a valid database array the same as in
@@ -712,7 +744,7 @@ public function submitConfirmIdConflictForm(array &$form, FormStateInterface $fo
   }
 
   /**
-   * Confirmation form showing available and missing migration paths.
+   * Confirmation form showing rollbacks, available and missing migration paths.
    *
    * The confirmation form uses the source_module and destination_module
    * properties on the source, destination and field plugins as well as the
@@ -733,6 +765,17 @@ public function buildConfirmForm(array $form, FormStateInterface $form_state) {
 
     $form['actions']['submit']['#value'] = $this->t('Perform upgrade');
 
+    if ($form_state->getValue('upgrade_option') == static::MIGRATE_UPGRADE_ROLLBACK) {
+      $form_state->setStorage(['upgrade_option' => static::MIGRATE_UPGRADE_ROLLBACK]);
+      $form['rollback'] = [
+        '#markup' => $this->t('All previously-imported content, as well as configuration such as field definitions, will be removed.'),
+      ];
+      $form['actions']['submit']['#value'] = $this->t('Perform rollback');
+    }
+    else {
+      $form['actions']['submit']['#value'] = $this->t('Perform upgrade');
+    }
+
     $version = $form_state->get('version');
 
     // Get the source_module and destination_module for each migration.
@@ -923,25 +966,62 @@ public function buildConfirmForm(array $form, FormStateInterface $form_state) {
    */
   public function submitConfirmForm(array &$form, FormStateInterface $form_state) {
     $storage = $form_state->getStorage();
-
-    $migrations = $storage['migrations'];
     $config['source_base_path'] = $storage['source_base_path'];
-    $batch = [
-      'title' => $this->t('Running upgrade'),
-      'progress_message' => '',
-      'operations' => [
-        [
-          [MigrateUpgradeImportBatch::class, 'run'],
-          [array_keys($migrations), $config],
+
+    if (isset($storage['upgrade_option']) && $storage['upgrade_option'] == static::MIGRATE_UPGRADE_ROLLBACK) {
+      $migrations = $this->pluginManager->createInstances([]);
+      // Assume we want all those tagged 'Drupal %'.
+      foreach ($migrations as $migration_id => $migration) {
+        $keep = FALSE;
+        $tags = $migration->get('migration_tags');
+        foreach ($tags as $tag) {
+          if (strpos($tag, 'Drupal ') === 0) {
+            $keep = TRUE;
+            break;
+          }
+        }
+        if (!$keep) {
+          unset($migrations[$migration_id]);
+        }
+      }
+      // Roll back in reverse order.
+      $migrations = array_reverse($migrations);
+      $batch = [
+        'title' => $this->t('Rolling back upgrade'),
+        'progress_message' => '',
+        'operations' => [
+          [
+            [MigrateUpgradeRollbackBatch::class, 'run'],
+            [array_keys($migrations), $config],
+          ],
         ],
-      ],
-      'finished' => [
-        MigrateUpgradeImportBatch::class, 'finished',
-      ],
-    ];
-    batch_set($batch);
-    $form_state->setRedirect('<front>');
-    $this->state->set('migrate_drupal_ui.performed', REQUEST_TIME);
+        'finished' => [
+          MigrateUpgradeRollbackBatch::class, 'finished',
+        ],
+      ];
+      batch_set($batch);
+      $form_state->setRedirect('migrate_drupal_ui.upgrade');
+      $this->state->delete('migrate_drupal_ui.performed');
+    }
+    else {
+      $migrations = $storage['migrations'];
+      $batch = [
+        'title' => $this->t('Running upgrade'),
+        'progress_message' => '',
+        'operations' => [
+          [
+            [MigrateUpgradeImportBatch::class, 'run'],
+            [array_keys($migrations), $config],
+          ],
+        ],
+        'finished' => [
+          MigrateUpgradeImportBatch::class, 'finished',
+        ],
+      ];
+      batch_set($batch);
+      $form_state->setRedirect('<front>');
+      $this->state->set('migrate_drupal_ui.performed', REQUEST_TIME);
+    }
   }
 
   /**
@@ -960,7 +1040,12 @@ protected function getDatabaseTypes() {
    * {@inheritdoc}
    */
   public function getQuestion() {
-    return $this->t('Upgrade analysis report');
+    if ($this->state->get('migrate_drupal_ui.performed')) {
+      return $this->t('Are you sure you want to rollback the migration?');
+    }
+    else {
+      return $this->t('Upgrade analysis report');
+    }
   }
 
   /**
@@ -976,7 +1061,6 @@ public function getCancelUrl() {
   public function getDescription() {
     // The description is added by the buildConfirmForm() method.
     // @see \Drupal\migrate_drupal_ui\Form\MigrateUpgradeForm::buildConfirmForm()
-    return;
   }
 
   /**
