diff --git a/core/includes/update.inc b/core/includes/update.inc
index 9e3a7a4e3445d2c64ba724942480575351131f5e..5254743c4fa10dab41977534db7a409df19b4c7c 100644
--- a/core/includes/update.inc
+++ b/core/includes/update.inc
@@ -320,14 +320,6 @@ function update_get_update_list() {
     // Otherwise, get the list of updates defined by this module.
     $updates = drupal_get_schema_versions($module);
     if ($updates !== FALSE) {
-      // \Drupal::moduleHandler()->invoke() returns NULL for non-existing hooks,
-      // so if no updates are removed, it will == 0.
-      $last_removed = \Drupal::moduleHandler()->invoke($module, 'update_last_removed');
-      if ($schema_version < $last_removed) {
-        $ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. Its schema version is ' . $schema_version . '. Updates up to and including ' . $last_removed . ' have been removed in this release. In order to update <em>' . $module . '</em> module, you will first <a href="https://www.drupal.org/upgrade">need to upgrade</a> to the last version in which these updates were available.';
-        continue;
-      }
-
       foreach ($updates as $update) {
         if ($update == \Drupal::CORE_MINIMUM_SCHEMA_VERSION) {
           $ret[$module]['warning'] = '<em>' . $module . '</em> module cannot be updated. It contains an update numbered as ' . \Drupal::CORE_MINIMUM_SCHEMA_VERSION . ' which is reserved for the earliest installation of a module in Drupal ' . \Drupal::CORE_COMPATIBILITY . ', before any updates. In order to update <em>' . $module . '</em> module, you will need to install a version of the module with valid updates.';
diff --git a/core/modules/rest/tests/fixtures/update/rest-module-installed.php b/core/modules/rest/tests/fixtures/update/rest-module-installed.php
index d9714e393fc28dc47e79427a0368e8091c3c0c1a..ae54885d0ae278763e91f62cbf37dd1ceed790f9 100644
--- a/core/modules/rest/tests/fixtures/update/rest-module-installed.php
+++ b/core/modules/rest/tests/fixtures/update/rest-module-installed.php
@@ -17,6 +17,13 @@
     'value' => 'i:8401;',
   ])
   ->execute();
+$connection->insert('key_value')
+  ->fields([
+    'collection' => 'system.schema',
+    'name' => 'serialization',
+    'value' => 'i:8401;',
+  ])
+  ->execute();
 
 // Update core.extension.
 $extensions = $connection->select('config')
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 5903461c9a2ad5859b971efdaec3265f28953308..a1ef14e53cce1fac59a65f018c7e858096cd6fdf 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1046,6 +1046,54 @@ function system_requirements($phase) {
     }
   }
 
+  // Ensure that no module has a current schema version that is lower than the
+  // one that was last removed.
+  if ($phase == 'update') {
+    $module_handler = \Drupal::moduleHandler();
+    $module_list = [];
+    foreach ($module_handler->getImplementations('update_last_removed') as $module) {
+      $last_removed = $module_handler->invoke($module, 'update_last_removed');
+      if ($last_removed && $last_removed > drupal_get_installed_schema_version($module)) {
+
+        /** @var \Drupal\Core\Extension\Extension $module_info */
+        $module_info = \Drupal::service('extension.list.module')->get($module);
+        $module_list[$module] = [
+          'name' => $module_info->info['name'],
+          'last_removed' => $last_removed,
+          'installed_version' => drupal_get_installed_schema_version($module),
+        ];
+      }
+    }
+
+    // If system.module is in the list then only show a specific message for
+    // Drupal core, otherwise show a more generic message for each module.
+    if (isset($module_list['system'])) {
+      $requirements['system_update_last_removed'] = [
+        'title' => t('The version of Drupal you are trying to update from is too old'),
+        'description' => t('Updating to Drupal @current_major is only supported from Drupal version @required_min_version or higher. If you are trying to update from an older version, first update to the latest version of Drupal @previous_major. (<a href=":url">Drupal 9 upgrade guide</a>)', [
+          '@current_major' => 9,
+          '@required_min_version' => '8.8.0',
+          '@previous_major' => 8,
+          ':url' => 'https://www.drupal.org/docs/9/how-to-prepare-your-drupal-7-or-8-site-for-drupal-9/upgrading-a-drupal-8-site-to-drupal-9',
+        ]),
+        'severity' => REQUIREMENT_ERROR,
+      ];
+    }
+    else {
+      foreach ($module_list as $module => $data) {
+        $requirements[$module . '_update_last_removed'] = [
+          'title' => t('Unsupported schema version: @module', ['@module' => $data['name']]),
+          'description' => t('The installed version of the %module module is too old to update. Update to an intermediate version first (last removed version: @last_removed_version, installed version: @installed_version).', [
+            '%module' => $data['name'],
+            '@last_removed_version' => $data['last_removed'],
+            '@installed_version' => $data['installed_version'],
+          ]),
+          'severity' => REQUIREMENT_ERROR,
+        ];
+      }
+    }
+  }
+
   return $requirements;
 }
 
@@ -1263,11 +1311,3 @@ function system_update_8901() {
     }
   }
 }
-
-/**
- * Ensures that Drupal is updated from a supported version.
- */
-function system_update_9000() {
-  // See update_get_update_list(), there needs to be at least one update
-  // function to check for the last removed schema version.
-}
diff --git a/core/modules/system/tests/modules/update_test_last_removed/update_test_last_removed.info.yml b/core/modules/system/tests/modules/update_test_last_removed/update_test_last_removed.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f4828a7807347361c85cff28b887f38a90dc9b72
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_last_removed/update_test_last_removed.info.yml
@@ -0,0 +1,5 @@
+name: 'Update test with update_last_removed implementation'
+type: module
+description: 'Support module for update testing.'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/update_test_last_removed/update_test_last_removed.install b/core/modules/system/tests/modules/update_test_last_removed/update_test_last_removed.install
new file mode 100644
index 0000000000000000000000000000000000000000..7b9700fca0ec3a2ebeb2540d25804b43f8a4dc22
--- /dev/null
+++ b/core/modules/system/tests/modules/update_test_last_removed/update_test_last_removed.install
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the update_test_invalid_hook module.
+ */
+
+/**
+ * Implements hook_update_last_removed()
+ */
+function update_test_last_removed_update_last_removed() {
+  return 8002;
+}
+
+/**
+ * Dummy update function to run during the tests.
+ */
+function update_test_last_removed_update_8003() {
+  return 'The update_test_last_removed_update_8003() update was executed successfully.';
+}
diff --git a/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..fc34b6adb9129e7b7b50e478262b9caa1950edcd
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/UpdateSystem/UpdatePathLastRemovedTest.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\UpdateSystem;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\UpdatePathTestTrait;
+
+/**
+ * Modules can define their last removed update function.
+ *
+ * @group system
+ */
+class UpdatePathLastRemovedTest extends BrowserTestBase {
+  use UpdatePathTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['update_test_last_removed'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * URL for the upgrade script.
+   *
+   * @var string
+   */
+  protected $updateUrl;
+
+  /**
+   * A user account with upgrade permission.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $updateUser;
+
+  protected function setUp() {
+    parent::setUp();
+    require_once $this->root . '/core/includes/update.inc';
+
+    $this->updateUrl = Url::fromRoute('system.db_update');
+    $this->updateUser = $this->drupalCreateUser(['administer software updates']);
+  }
+
+  /**
+   * Test that a module with a too old schema version can not be updated.
+   */
+  public function testLastRemovedVersion() {
+    drupal_set_installed_schema_version('update_test_last_removed', 8000);
+    drupal_set_installed_schema_version('system', 8804);
+
+    // Access the update page with too old versions for system and the test
+    // module, only the generic core message should be shown.
+    $this->drupalLogin($this->updateUser);
+    $this->drupalGet($this->updateUrl);
+    $assert_session = $this->assertSession();
+    $assert_session->pageTextContains('Requirements problem');
+    $assert_session->pageTextContains('The version of Drupal you are trying to update from is too old');
+    $assert_session->pageTextContains('Updating to Drupal 9 is only supported from Drupal version 8.8.0 or higher. If you are trying to update from an older version, first update to the latest version of Drupal 8');
+    $assert_session->pageTextNotContains('Unsupported schema version: Update test with update_last_removed implementation');
+
+    $assert_session->linkNotExists('Continue');
+
+    // Update the installed version of system and then assert that now,
+    // the test module is shown instead.
+    drupal_set_installed_schema_version('system', 8805);
+    $this->drupalGet($this->updateUrl);
+
+    $assert_session->pageTextNotContains('The version of Drupal you are trying to update from is too old');
+
+    $assert_session->pageTextContains('Unsupported schema version: Update test with update_last_removed implementation');
+    $assert_session->pageTextContains('The installed version of the Update test with update_last_removed implementation module is too old to update. Update to an intermediate version first (last removed version: 8002, installed version: 8000).');
+    $assert_session->linkNotExists('Continue');
+
+    // Set the expected schema version for the node and test module, updates are
+    // successful now.
+    drupal_set_installed_schema_version('update_test_last_removed', 8002);
+
+    $this->runUpdates();
+    $this->assertEquals(8003, drupal_get_installed_schema_version('update_test_last_removed'));
+
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
index 61facab0bd2659dfe472e0dd907c448fcefe3994..8c3fc723d50439f056613d165f0a23e78281203f 100644
--- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php
+++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
@@ -59,6 +59,7 @@ protected function runUpdates($update_url = NULL) {
       }
 
       // Ensure that there are no pending updates.
+      drupal_get_installed_schema_version(NULL, TRUE);
       foreach (['update', 'post_update'] as $update_type) {
         switch ($update_type) {
           case 'update':