diff --git a/core/modules/node/src/Plugin/views/wizard/Node.php b/core/modules/node/src/Plugin/views/wizard/Node.php
index 07b176ccd4..e5844100de 100644
--- a/core/modules/node/src/Plugin/views/wizard/Node.php
+++ b/core/modules/node/src/Plugin/views/wizard/Node.php
@@ -167,7 +167,7 @@ protected function defaultDisplayFiltersUser(array $form, FormStateInterface $fo
         'table' => 'taxonomy_index',
         'field' => 'tid',
         'value' => $tids,
-        'vid' => $vid,
+        'vid' => [$vid],
         'plugin_id' => 'taxonomy_index_tid',
       ];
       // If the user entered more than one valid term in the autocomplete
diff --git a/core/modules/taxonomy/config/schema/taxonomy.views.schema.yml b/core/modules/taxonomy/config/schema/taxonomy.views.schema.yml
index 9ec9758185..98be95b22d 100644
--- a/core/modules/taxonomy/config/schema/taxonomy.views.schema.yml
+++ b/core/modules/taxonomy/config/schema/taxonomy.views.schema.yml
@@ -120,9 +120,12 @@ views.filter.taxonomy_index_tid:
   type: views.filter.many_to_one
   label: 'Taxonomy term ID'
   mapping:
-    vid:
-      type: string
-      label: 'Vocabulary'
+    vids:
+      type: sequence
+      label: 'Vocabularies'
+      sequence:
+        type: string
+        label: 'Vocabulary'
     type:
       type: string
       label: 'Selection type'
diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
index 44133826f1..3c08baa3a6 100644
--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
+++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTid.php
@@ -3,10 +3,12 @@
 namespace Drupal\taxonomy\Plugin\views\filter;
 
 use Drupal\Core\Entity\Element\EntityAutocomplete;
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\taxonomy\Entity\Term;
 use Drupal\taxonomy\TermStorageInterface;
+use Drupal\taxonomy\VocabularyInterface;
 use Drupal\taxonomy\VocabularyStorageInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\DisplayPluginBase;
@@ -22,7 +24,9 @@
  */
 class TaxonomyIndexTid extends ManyToOne {
 
-  // Stores the exposed input for this filter.
+  /**
+   * Stores the exposed input for this filter.
+   */
   public $validated_exposed_input = NULL;
 
   /**
@@ -39,6 +43,13 @@ class TaxonomyIndexTid extends ManyToOne {
    */
   protected $termStorage;
 
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
   /**
    * The current user.
    *
@@ -61,11 +72,18 @@ class TaxonomyIndexTid extends ManyToOne {
    *   The term storage.
    * @param \Drupal\Core\Session\AccountInterface $current_user
    *   The current user.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository service.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage, AccountInterface $current_user = NULL) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage, AccountInterface $current_user = NULL, EntityRepositoryInterface $entity_repository = NULL) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->vocabularyStorage = $vocabulary_storage;
     $this->termStorage = $term_storage;
+    if (!$entity_repository) {
+      @trigger_error('Calling TaxonomyIndexTid::__construct() without the $entity_repository argument is deprecated in drupal:9.1.0 and the $entity_repository argument will be required in drupal:10.0.0. See https://www.drupal.org/node/3162414', E_USER_DEPRECATED);
+      $entity_repository = \Drupal::service('entity.repository');
+    }
+    $this->entityRepository = $entity_repository;
     if (!$current_user) {
       @trigger_error('The current_user service must be passed to ' . __NAMESPACE__ . '\TaxonomyIndexTid::__construct(). It was added in drupal:8.9.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
       $current_user = \Drupal::service('current_user');
@@ -83,7 +101,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_definition,
       $container->get('entity_type.manager')->getStorage('taxonomy_vocabulary'),
       $container->get('entity_type.manager')->getStorage('taxonomy_term'),
-      $container->get('current_user')
+      $container->get('current_user'),
+      $container->get('entity.repository')
     );
   }
 
@@ -94,10 +113,13 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$o
     parent::init($view, $display, $options);
 
     if (!empty($this->definition['vocabulary'])) {
-      $this->options['vid'] = $this->definition['vocabulary'];
+      $this->options['vids'] = [$this->definition['vocabulary']];
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function hasExtraOptions() {
     return TRUE;
   }
@@ -109,39 +131,38 @@ public function getValueOptions() {
     return $this->valueOptions;
   }
 
+  /**
+   * {@inheritdoc}
+   */
   protected function defineOptions() {
     $options = parent::defineOptions();
 
     $options['type'] = ['default' => 'textfield'];
     $options['limit'] = ['default' => TRUE];
-    $options['vid'] = ['default' => ''];
+    $options['vids'] = ['default' => []];
     $options['hierarchy'] = ['default' => FALSE];
     $options['error_message'] = ['default' => TRUE];
 
     return $options;
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
-    $vocabularies = $this->vocabularyStorage->loadMultiple();
-    $options = [];
-    foreach ($vocabularies as $voc) {
-      $options[$voc->id()] = $voc->label();
-    }
 
     if ($this->options['limit']) {
       // We only do this when the form is displayed.
-      if (empty($this->options['vid'])) {
-        $first_vocabulary = reset($vocabularies);
-        $this->options['vid'] = $first_vocabulary->id();
-      }
+      $vocabularies = $this->vocabularyStorage->loadMultiple();
 
       if (empty($this->definition['vocabulary'])) {
-        $form['vid'] = [
-          '#type' => 'radios',
+        $form['vids'] = [
+          '#type' => 'checkboxes',
           '#title' => $this->t('Vocabulary'),
-          '#options' => $options,
+          '#options' => $this->getVocabularyLabels($vocabularies),
           '#description' => $this->t('Select which vocabulary to show terms for in the regular options.'),
-          '#default_value' => $this->options['vid'],
+          '#default_value' => $this->options['vids'],
+          '#required' => TRUE,
         ];
       }
     }
@@ -149,7 +170,10 @@ public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
     $form['type'] = [
       '#type' => 'radios',
       '#title' => $this->t('Selection type'),
-      '#options' => ['select' => $this->t('Dropdown'), 'textfield' => $this->t('Autocomplete')],
+      '#options' => [
+        'select' => $this->t('Dropdown'),
+        'textfield' => $this->t('Autocomplete'),
+      ],
       '#default_value' => $this->options['type'],
     ];
 
@@ -165,19 +189,40 @@ public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function submitExtraOptionsForm($form, FormStateInterface $form_state) {
+    $vids = $form_state->getValue(['options', 'vids']);
+    $form_state->setValue(['options', 'vids'], array_keys(array_filter($vids)));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   protected function valueForm(&$form, FormStateInterface $form_state) {
-    $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
-    if (empty($vocabulary) && $this->options['limit']) {
+    $vocabularies = $this->vocabularyStorage->loadMultiple($this->options['vids']);
+    if (empty($vocabularies) && $this->options['limit']) {
       $form['markup'] = [
-        '#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid vocabulary is selected. Please change it in the options.') . '</div>',
+        '#markup' => '<div class="js-form-item form-item">' . $this->t('Invalid or no vocabularies are selected. Please select valid vocabularies in filter settings.') . '</div>',
       ];
       return;
     }
 
+    $form['value'] = [
+      '#title' => $this->options['limit'] ? $this->formatPlural(
+        count($vocabularies),
+        'Select terms from vocabulary @vocabs',
+        'Select terms from vocabularies @vocabs',
+        [
+          '@vocabs' => "'" . implode("', '", $this->getVocabularyLabels($vocabularies)) . "'",
+        ]
+      ) : $this->t('Select terms'),
+    ];
+
     if ($this->options['type'] == 'textfield') {
-      $terms = $this->value ? Term::loadMultiple(($this->value)) : [];
-      $form['value'] = [
-        '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
+      $terms = $this->value ? $this->termStorage->loadMultiple($this->value) : [];
+      $form['value'] += [
         '#type' => 'textfield',
         '#default_value' => EntityAutocomplete::getEntityLabels($terms),
       ];
@@ -185,30 +230,36 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
       if ($this->options['limit']) {
         $form['value']['#type'] = 'entity_autocomplete';
         $form['value']['#target_type'] = 'taxonomy_term';
-        $form['value']['#selection_settings']['target_bundles'] = [$vocabulary->id()];
+        $form['value']['#selection_settings']['target_bundles'] = array_keys($vocabularies);
         $form['value']['#tags'] = TRUE;
         $form['value']['#process_default_value'] = FALSE;
       }
     }
     else {
+      $options = [];
       if (!empty($this->options['hierarchy']) && $this->options['limit']) {
-        $tree = $this->termStorage->loadTree($vocabulary->id(), 0, NULL, TRUE);
-        $options = [];
-
-        if ($tree) {
-          foreach ($tree as $term) {
-            if (!$term->isPublished() && !$this->currentUser->hasPermission('administer taxonomy')) {
-              continue;
+        foreach ($vocabularies as $vocabulary) {
+          $tree = $this->termStorage->loadTree($vocabulary->id(), 0, NULL, TRUE);
+          if ($tree) {
+            foreach ($tree as $term) {
+              if (!$term->isPublished() && !$this->currentUser->hasPermission('administer taxonomy')) {
+                continue;
+              }
+              $choice = new \stdClass();
+              $choice->option = [
+                $term->id() => str_repeat('-', $term->depth) .
+                $this->entityRepository
+                  ->getTranslationFromContext($term)
+                  ->label(),
+              ];
+              $options[] = $choice;
             }
-            $choice = new \stdClass();
-            $choice->option = [$term->id() => str_repeat('-', $term->depth) . \Drupal::service('entity.repository')->getTranslationFromContext($term)->label()];
-            $options[] = $choice;
           }
         }
       }
       else {
         $options = [];
-        $query = \Drupal::entityQuery('taxonomy_term')
+        $query = $this->termStorage->getQuery()
           ->accessCheck(TRUE)
           // @todo Sorting on vocabulary properties -
           //   https://www.drupal.org/node/1821274.
@@ -219,15 +270,15 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
           $query->condition('status', 1);
         }
         if ($this->options['limit']) {
-          $query->condition('vid', $vocabulary->id());
+          $query->condition('vid', $this->options['vids'], 'IN');
         }
-        $terms = Term::loadMultiple($query->execute());
+        $terms = $this->termStorage->loadMultiple($query->execute());
         foreach ($terms as $term) {
-          $options[$term->id()] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
+          $options[$term->id()] = $this->entityRepository->getTranslationFromContext($term)->label();
         }
       }
 
-      $default_value = (array) $this->value;
+      $default_value = $this->value;
 
       if ($exposed = $form_state->get('exposed')) {
         $identifier = $this->options['expose']['identifier'];
@@ -259,9 +310,8 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
           }
         }
       }
-      $form['value'] = [
+      $form['value'] += [
         '#type' => 'select',
-        '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
         '#multiple' => TRUE,
         '#options' => $options,
         '#size' => min(9, count($options)),
@@ -276,7 +326,7 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
     }
 
     if (!$form_state->get('exposed')) {
-      // Retain the helper option
+      // Retain the helper option.
       $this->helper->buildOptionsForm($form, $form_state);
 
       // Show help text if not exposed to end users.
@@ -284,6 +334,9 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
   protected function valueValidate($form, FormStateInterface $form_state) {
     // We only validate if they've chosen the text field style.
     if ($this->options['type'] != 'textfield') {
@@ -299,6 +352,9 @@ protected function valueValidate($form, FormStateInterface $form_state) {
     $form_state->setValue(['options', 'value'], $tids);
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function acceptExposedInput($input) {
     if (empty($this->options['exposed'])) {
       return TRUE;
@@ -311,7 +367,7 @@ public function acceptExposedInput($input) {
     }
 
     // If view is an attachment and is inheriting exposed filters, then assume
-    // exposed input has already been validated
+    // exposed input has already been validated.
     if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
       $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
     }
@@ -338,6 +394,9 @@ public function acceptExposedInput($input) {
     return $rc;
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function validateExposed(&$form, FormStateInterface $form_state) {
     if (empty($this->options['exposed'])) {
       return;
@@ -364,10 +423,16 @@ public function validateExposed(&$form, FormStateInterface $form_state) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
   protected function valueSubmit($form, FormStateInterface $form_state) {
-    // prevent array_filter from messing up our arrays in parent submit.
+    // Prevent array_filter from messing up our arrays in parent submit.
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function buildExposeForm(&$form, FormStateInterface $form_state) {
     parent::buildExposeForm($form, $form_state);
     if ($this->options['type'] != 'select') {
@@ -380,15 +445,20 @@ public function buildExposeForm(&$form, FormStateInterface $form_state) {
     ];
   }
 
+  /**
+   * {@inheritdoc}
+   */
   public function adminSummary() {
-    // set up $this->valueOptions for the parent summary
+    // Set up $this->valueOptions for the parent summary.
     $this->valueOptions = [];
 
     if ($this->value) {
       $this->value = array_filter($this->value);
       $terms = Term::loadMultiple($this->value);
       foreach ($terms as $term) {
-        $this->valueOptions[$term->id()] = \Drupal::service('entity.repository')->getTranslationFromContext($term)->label();
+        $this->valueOptions[$term->id()] = $this->entityRepository
+          ->getTranslationFromContext($term)
+          ->label();
       }
     }
     return parent::adminSummary();
@@ -413,9 +483,10 @@ public function getCacheContexts() {
   public function calculateDependencies() {
     $dependencies = parent::calculateDependencies();
 
-    $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
-    $dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
-
+    $vocabularies = $this->vocabularyStorage->loadMultiple($this->options['vids']);
+    foreach ($vocabularies as $vocabulary) {
+      $dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
+    }
     foreach ($this->termStorage->loadMultiple($this->options['value']) as $term) {
       $dependencies[$term->getConfigDependencyKey()][] = $term->getConfigDependencyName();
     }
@@ -423,4 +494,19 @@ public function calculateDependencies() {
     return $dependencies;
   }
 
+  /**
+   * Returns a list of vocabulary labels keyed by vocabulary ID.
+   *
+   * @param array $vocabularies
+   *   An associative array of vocabulary entities, keyed by vocabulary ID.
+   *
+   * @return array
+   *   Associative array of vocabulary labels keyed by vocabulary ID.
+   */
+  protected function getVocabularyLabels(array $vocabularies): array {
+    return array_map(function (VocabularyInterface $vocabulary): string {
+      return $vocabulary->label();
+    }, $vocabularies);
+  }
+
 }
diff --git a/core/modules/taxonomy/taxonomy.post_update.php b/core/modules/taxonomy/taxonomy.post_update.php
index 2e18d17ad8..eb0d2bdd90 100644
--- a/core/modules/taxonomy/taxonomy.post_update.php
+++ b/core/modules/taxonomy/taxonomy.post_update.php
@@ -5,6 +5,10 @@
  * Post update functions for Taxonomy.
  */
 
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\views\ViewEntityInterface;
+use Drupal\views\ViewsConfigUpdater;
+
 /**
  * Implements hook_removed_post_updates().
  */
@@ -18,3 +22,20 @@ function taxonomy_removed_post_updates() {
     'taxonomy_post_update_configure_status_field_widget' => '9.0.0',
   ];
 }
+
+/**
+ * Allow multiple vocabularies for views using the taxonomy term ID filter.
+ */
+function taxonomy_post_update_multiple_vocabularies_filter(?array &$sandbox = NULL): void {
+  // If Views is not installed, there is nothing to do.
+  if (!\Drupal::moduleHandler()->moduleExists('views')) {
+    return;
+  }
+
+  /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */
+  $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
+  \Drupal::classResolver(ConfigEntityUpdater::class)
+    ->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool {
+      return $view_config_updater->needsTidFilterWithMultipleVocabulariesUpdate($view);
+    });
+}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid.yml
index be15a8f3e7..a86492817a 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid.yml
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid.yml
@@ -165,7 +165,8 @@ display:
           reduce_duplicates: false
           type: select
           limit: true
-          vid: tags
+          vids:
+            - tags
           hierarchy: true
           error_message: true
           plugin_id: taxonomy_index_tid
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid__non_existing_dependency.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid__non_existing_dependency.yml
index ea9f640813..ad91187217 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid__non_existing_dependency.yml
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid__non_existing_dependency.yml
@@ -166,7 +166,8 @@ display:
           reduce_duplicates: false
           type: select
           limit: true
-          vid: tags
+          vids:
+            - tags
           hierarchy: true
           error_message: true
           plugin_id: taxonomy_index_tid
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid_depth.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid_depth.yml
index aeaae4bda8..7adf4fda05 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid_depth.yml
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid_depth.yml
@@ -164,7 +164,8 @@ display:
           reduce_duplicates: false
           type: select
           limit: true
-          vid: views_testing_tags
+          vids:
+            - views_testing_tags
           hierarchy: true
           depth: -2
           error_message: true
diff --git a/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermIdFilterUpdateTest.php b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermIdFilterUpdateTest.php
new file mode 100644
index 0000000000..68428e2f10
--- /dev/null
+++ b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyTermIdFilterUpdateTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\taxonomy\Functional\Update;
+
+use Drupal\Core\Serialization\Yaml;
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests update of term ID filter handlers to allow multiple vocabularies.
+ *
+ * @group taxonomy
+ * @group legacy
+ *
+ * @see taxonomy_post_update_multiple_vocabularies_filter()
+ */
+class TaxonomyTermIdFilterUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles(): void {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests that field handlers are updated properly.
+   */
+  public function testPostUpdateTaxonomyIndexFilterMultipleVocabularies(): void {
+    // Prepare the test view with the old schema.
+    $view_as_array = Yaml::decode(file_get_contents(__DIR__ . '/../../../modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid.yml'));
+    $view_as_array['display']['default']['display_options']['filters']['tid']['vid'] = 'tags';
+    unset($view_as_array['display']['default']['display_options']['filters']['tid']['vids']);
+    $db = \Drupal::database();
+    $db->insert('config')
+      ->fields([
+        'collection' => '',
+        'name' => 'views.view.test_filter_taxonomy_index_tid',
+        'data' => serialize($view_as_array),
+      ])->execute();
+
+    $path = 'display.default.display_options.filters.tid';
+    $config_factory = \Drupal::configFactory();
+    $view_as_config = $config_factory->get('views.view.test_filter_taxonomy_index_tid');
+
+    // Check that, prior running updates, only the legacy 'vid' key exists.
+    $this->assertSame('tags', $view_as_config->get("{$path}.vid"));
+    $this->assertArrayNotHasKey('vids', $view_as_config->get($path));
+
+    $this->runUpdates();
+
+    $view_as_config = $config_factory->get('views.view.test_filter_taxonomy_index_tid');
+
+    // Check that, after running updates, only the new 'vids' key exists.
+    $this->assertSame(['tags'], $view_as_config->get("{$path}.vids"));
+    $this->assertArrayNotHasKey('vid', $view_as_config->get($path));
+  }
+
+}
diff --git a/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyIndexTidUiTest.php b/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyIndexTidUiTest.php
index f447af3fb1..4617b227dc 100644
--- a/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyIndexTidUiTest.php
+++ b/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyIndexTidUiTest.php
@@ -24,7 +24,10 @@ class TaxonomyIndexTidUiTest extends UITestBase {
    *
    * @var array
    */
-  public static $testViews = ['test_filter_taxonomy_index_tid', 'test_taxonomy_term_name'];
+  public static $testViews = [
+    'test_filter_taxonomy_index_tid',
+    'test_taxonomy_term_name',
+  ];
 
   /**
    * {@inheritdoc}
@@ -62,11 +65,35 @@ protected function setUp($import_test_views = TRUE): void {
       'administer views',
     ]);
     $this->drupalLogin($this->adminUser);
+    $this->terms = $this->createVocabularyAndTerms('tags');
+    ViewTestData::createTestViews(get_class($this), ['taxonomy_test_views']);
 
     Vocabulary::create([
-      'vid' => 'tags',
-      'name' => 'Tags',
+      'vid' => 'empty_vocabulary',
+      'name' => 'Empty Vocabulary',
     ])->save();
+  }
+
+  /**
+   * Creates a vocabulary and terms for it.
+   *
+   * @param string $vocab_id
+   *   The vocabulary ID.
+   *
+   * @return \Drupal\taxonomy\TermInterface[][]
+   *   The terms.
+   *
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   */
+  protected function createVocabularyAndTerms($vocab_id) {
+
+    $vocab = Vocabulary::load($vocab_id);
+    if (!$vocab) {
+      Vocabulary::create([
+        'vid' => $vocab_id,
+        'name' => 'Test Vocabulary ' . $vocab_id,
+      ])->save();
+    }
 
     // Setup a hierarchy which looks like this:
     // term 0.0
@@ -74,23 +101,20 @@ protected function setUp($import_test_views = TRUE): void {
     // - term 1.1
     // term 2.0
     // - term 2.1
-    // - term 2.2
+    // - term 2.2.
+    $terms = [];
     for ($i = 0; $i < 3; $i++) {
       for ($j = 0; $j <= $i; $j++) {
-        $this->terms[$i][$j] = $term = Term::create([
-          'vid' => 'tags',
+        $term = Term::create([
+          'vid' => $vocab_id,
           'name' => "Term $i.$j",
           'parent' => isset($this->terms[$i][0]) ? $this->terms[$i][0]->id() : 0,
         ]);
         $term->save();
+        $terms[$i][$j] = $term;
       }
     }
-    ViewTestData::createTestViews(static::class, ['taxonomy_test_views']);
-
-    Vocabulary::create([
-      'vid' => 'empty_vocabulary',
-      'name' => 'Empty Vocabulary',
-    ])->save();
+    return $terms;
   }
 
   /**
@@ -99,6 +123,8 @@ protected function setUp($import_test_views = TRUE): void {
   public function testFilterUI() {
     $this->drupalGet('admin/structure/views/nojs/handler/test_filter_taxonomy_index_tid/default/filter/tid');
 
+    $this->xpath('//select[@id="edit-options-value"]', []);
+
     $result = $this->assertSession()->selectExists('edit-options-value')->findAll('css', 'option');
 
     // Ensure that the expected hierarchy is available in the UI.
@@ -109,8 +135,8 @@ public function testFilterUI() {
         $prefix = $this->terms[$i][$j]->parent->target_id ? '-' : '';
         $tid = $option->getAttribute('value');
 
-        $this->assertEqual($prefix . $this->terms[$i][$j]->getName(), $option->getText());
-        $this->assertEqual($this->terms[$i][$j]->id(), $tid);
+        $this->assertEquals($prefix . $this->terms[$i][$j]->getName(), $option->getText());
+        $this->assertEquals($this->terms[$i][$j]->id(), $tid);
       }
     }
 
@@ -140,6 +166,79 @@ public function testFilterUI() {
     $this->assertSame($expected, $view->calculateDependencies()->getDependencies());
   }
 
+  /**
+   * Test filter UI with multiple vocabularies.
+   */
+  public function testFilterUIWithMultipleVocabularies() {
+    $terms2 = $this->createVocabularyAndTerms('tags2');
+    $node_type = $this->drupalCreateContentType(['type' => 'page']);
+    // Create the tag field itself.
+    $field_name = 'taxonomy_tags';
+    $this->createEntityReferenceField('node', $node_type->id(), $field_name, NULL, 'taxonomy_term');
+    $node0 = $this->drupalCreateNode([
+      'type' => 'page',
+      'taxonomy_tags' => $this->terms[0][0]->id(),
+    ]);
+    $node0->save();
+    $node1 = $this->drupalCreateNode([
+      'type' => 'page',
+      'taxonomy_tags' => $terms2[0][0]->id(),
+    ]);
+    $node1->save();
+    $node2 = $this->drupalCreateNode([
+      'type' => 'page',
+      'taxonomy_tags' => [$this->terms[0][0]->id(), $terms2[0][0]->id()],
+    ]);
+    $node2->save();
+    $form_id = 'some-form-id';
+    $edit = [
+      'options[vids][tags]' => TRUE,
+      'options[vids][tags2]' => TRUE,
+      'options[type]' => 'textfield',
+    ];
+    $this->submitForm($edit, 'Apply', $form_id);
+    $this->submitForm([], 'Expose filter', $form_id);
+    $edit = [
+      'options[operator]' => 'and',
+      'options[value]' => '',
+      'options[reduce_duplicates]' => TRUE,
+    ];
+    $this->submitForm($edit, 'Apply', $form_id);
+    $this->submitForm([], 'Save', $form_id);
+    $this->drupalGet('test-filter-taxonomy-index-tid', ['query' => ['tid' => '']]);
+    $xpath = $this->xpath('//div[@class="view-content"]//a');
+    $this->assertSame(3, count($xpath));
+    $this->drupalGet('test-filter-taxonomy-index-tid', ['query' => ['tid' => "t1 ({$this->terms[0][0]->id()})"]]);
+    $xpath = $this->xpath('//div[@class="view-content"]//a');
+    $this->assertSame(2, count($xpath));
+    $xpath = $this->xpath('//div[@class="view-content"]//a[@href=:href]', [
+      ':href' => $node0->toUrl()->toString(),
+    ]);
+    $this->assertSame(1, count($xpath));
+    $xpath = $this->xpath('//div[@class="view-content"]//a[@href=:href]', [
+      ':href' => $node2->toUrl()->toString(),
+    ]);
+    $this->assertSame(1, count($xpath));
+    $this->drupalGet('test-filter-taxonomy-index-tid', ['query' => ['tid' => "t2 ({$terms2[0][0]->id()})"]]);
+    $xpath = $this->xpath('//div[@class="view-content"]//a');
+    $this->assertSame(2, count($xpath));
+    $xpath = $this->xpath('//div[@class="view-content"]//a[@href=:href]', [
+      ':href' => $node1->toUrl()->toString(),
+    ]);
+    $this->assertSame(1, count($xpath));
+    $xpath = $this->xpath('//div[@class="view-content"]//a[@href=:href]', [
+      ':href' => $node2->toUrl()->toString(),
+    ]);
+    $this->assertSame(1, count($xpath));
+    $this->drupalGet('test-filter-taxonomy-index-tid', ['query' => ['tid' => "t1 ({$this->terms[0][0]->id()}), t2 ({$terms2[0][0]->id()})"]]);
+    $xpath = $this->xpath('//div[@class="view-content"]//a');
+    $this->assertSame(1, count($xpath));
+    $xpath = $this->xpath('//div[@class="view-content"]//a[@href=:href]', [
+      ':href' => $node2->toUrl()->toString(),
+    ]);
+    $this->assertSame(1, count($xpath));
+  }
+
   /**
    * Tests exposed taxonomy filters.
    */
@@ -217,7 +316,11 @@ public function testExposedFilter() {
     ];
     $this->drupalPostForm('admin/structure/views/nojs/add-handler/test_taxonomy_term_name/default/filter', $edit, 'Add and configure filter criteria');
     // Select 'Empty Vocabulary' and 'Autocomplete' from the list of options.
-    $this->drupalPostForm('admin/structure/views/nojs/handler-extra/test_taxonomy_term_name/default/filter/tid', [], 'Apply and continue');
+    $edit = [
+      'options[vids][empty_vocabulary]' => TRUE,
+      'options[type]' => 'textfield',
+    ];
+    $this->drupalPostForm('admin/structure/views/nojs/handler-extra/test_taxonomy_term_name/default/filter/tid', $edit, 'Apply and continue');
     // Expose the filter.
     $edit = ['options[expose_button][checkbox][checkbox]' => TRUE];
     $this->drupalPostForm('admin/structure/views/nojs/handler/test_taxonomy_term_name/default/filter/tid', $edit, 'Expose filter');
diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php
index 88442d46dd..053899de2e 100644
--- a/core/modules/views/src/ViewsConfigUpdater.php
+++ b/core/modules/views/src/ViewsConfigUpdater.php
@@ -138,6 +138,9 @@ public function updateAll(ViewEntityInterface $view) {
       if ($this->processMultivalueBaseFieldHandler($handler, $handler_type, $key, $display_id, $view)) {
         $changed = TRUE;
       }
+      if ($this->processTidFilterWithMultipleVocabulariesHandler($handler, $handler_type)) {
+        $changed = TRUE;
+      }
       return $changed;
     });
   }
@@ -477,4 +480,39 @@ protected function mapOperatorFromSingleToMultiple($single_operator) {
     }
   }
 
+  /**
+   * Update taxonomy term ID filter handlers to allow multiple vocabularies.
+   *
+   * @param \Drupal\views\ViewEntityInterface $view
+   *   The View to update.
+   *
+   * @return bool
+   *   Whether the view was updated.
+   */
+  public function needsTidFilterWithMultipleVocabulariesUpdate(ViewEntityInterface $view): bool {
+    return $this->processDisplayHandlers($view, TRUE, function (array &$handler, string $handler_type) use ($view): bool {
+      return $this->processTidFilterWithMultipleVocabulariesHandler($handler, $handler_type);
+    });
+  }
+
+  /**
+   * Processes taxonomy term ID filter handlers to allow multiple vocabularies.
+   *
+   * @param array $handler
+   *   A display handler.
+   * @param string $handler_type
+   *   The handler type.
+   *
+   * @return bool
+   *   Whether the handler was updated.
+   */
+  protected function processTidFilterWithMultipleVocabulariesHandler(array &$handler, string $handler_type): bool {
+    if ($handler_type === 'filter' && isset($handler['plugin_id']) && $handler['plugin_id'] === 'taxonomy_index_tid' && empty($handler['vids']) && !empty($handler['vid'])) {
+      $handler['vids'] = [$handler['vid']];
+      unset($handler['vid']);
+      return TRUE;
+    }
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml
index 923c335cd1..73249e113c 100644
--- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml
@@ -107,7 +107,8 @@ display:
           reduce_duplicates: false
           type: select
           limit: true
-          vid: test_exposed_checkboxes
+          vids:
+            - test_exposed_checkboxes
           hierarchy: false
           error_message: true
           plugin_id: taxonomy_index_tid
diff --git a/core/tests/src/Functional/Update/TaxonomyTermIdFilterUpdateTest.php b/core/tests/src/Functional/Update/TaxonomyTermIdFilterUpdateTest.php
new file mode 100644
index 0000000000..68428e2f10
--- /dev/null
+++ b/core/tests/src/Functional/Update/TaxonomyTermIdFilterUpdateTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types = 1);
+
+namespace Drupal\Tests\taxonomy\Functional\Update;
+
+use Drupal\Core\Serialization\Yaml;
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests update of term ID filter handlers to allow multiple vocabularies.
+ *
+ * @group taxonomy
+ * @group legacy
+ *
+ * @see taxonomy_post_update_multiple_vocabularies_filter()
+ */
+class TaxonomyTermIdFilterUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles(): void {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.8.0.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests that field handlers are updated properly.
+   */
+  public function testPostUpdateTaxonomyIndexFilterMultipleVocabularies(): void {
+    // Prepare the test view with the old schema.
+    $view_as_array = Yaml::decode(file_get_contents(__DIR__ . '/../../../modules/taxonomy_test_views/test_views/views.view.test_filter_taxonomy_index_tid.yml'));
+    $view_as_array['display']['default']['display_options']['filters']['tid']['vid'] = 'tags';
+    unset($view_as_array['display']['default']['display_options']['filters']['tid']['vids']);
+    $db = \Drupal::database();
+    $db->insert('config')
+      ->fields([
+        'collection' => '',
+        'name' => 'views.view.test_filter_taxonomy_index_tid',
+        'data' => serialize($view_as_array),
+      ])->execute();
+
+    $path = 'display.default.display_options.filters.tid';
+    $config_factory = \Drupal::configFactory();
+    $view_as_config = $config_factory->get('views.view.test_filter_taxonomy_index_tid');
+
+    // Check that, prior running updates, only the legacy 'vid' key exists.
+    $this->assertSame('tags', $view_as_config->get("{$path}.vid"));
+    $this->assertArrayNotHasKey('vids', $view_as_config->get($path));
+
+    $this->runUpdates();
+
+    $view_as_config = $config_factory->get('views.view.test_filter_taxonomy_index_tid');
+
+    // Check that, after running updates, only the new 'vids' key exists.
+    $this->assertSame(['tags'], $view_as_config->get("{$path}.vids"));
+    $this->assertArrayNotHasKey('vid', $view_as_config->get($path));
+  }
+
+}
