diff --git a/modules/payment/src/Form/PaymentMethodEditForm.php b/modules/payment/src/Form/PaymentMethodEditForm.php
index 72d2c9e4..dc0e2f9d 100644
--- a/modules/payment/src/Form/PaymentMethodEditForm.php
+++ b/modules/payment/src/Form/PaymentMethodEditForm.php
@@ -26,10 +26,8 @@ class PaymentMethodEditForm extends EntityForm {
   /**
    * {@inheritdoc}
    */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    parent::validateForm($form, $form_state);
-
-    $this->entity = $form_state->getValue('payment_method');
+  public function buildEntity(array $form, FormStateInterface $form_state) {
+    return $form_state->getValue('payment_method');
   }
 
   /**
@@ -41,7 +39,7 @@ class PaymentMethodEditForm extends EntityForm {
       '%label' => $this->entity->label(),
       '@entity-type' => $this->entity->getEntityType()->getLowercaseLabel(),
     ]));
-    $form_state->setRedirect($this->entity->toUrl('collection'));
+    $form_state->setRedirectUrl($this->entity->toUrl('collection'));
   }
 
 }
diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php
index 1f3b2cae..03fe9262 100644
--- a/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php
+++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/SupportsUpdatingStoredPaymentMethodsInterface.php
@@ -10,7 +10,7 @@ use Drupal\commerce_payment\Entity\PaymentMethodInterface;
 interface SupportsUpdatingStoredPaymentMethodsInterface {
 
   /**
-   * Updates the given payment.
+   * Updates the given payment method.
    *
    * @param \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method
    *   The payment method.
diff --git a/modules/payment/src/PluginForm/PaymentMethodEditForm.php b/modules/payment/src/PluginForm/PaymentMethodEditForm.php
index 11dc962b..bf3dd75a 100644
--- a/modules/payment/src/PluginForm/PaymentMethodEditForm.php
+++ b/modules/payment/src/PluginForm/PaymentMethodEditForm.php
@@ -2,9 +2,191 @@
 
 namespace Drupal\commerce_payment\PluginForm;
 
-/**
- * @todo
- */
-class PaymentMethodEditForm extends PaymentMethodAddForm {
+use Drupal\commerce_payment\CreditCard;
+use Drupal\commerce_payment\Entity\PaymentMethodInterface;
+use Drupal\commerce_payment\Exception\DeclineException;
+use Drupal\commerce_payment\Exception\PaymentGatewayException;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+class PaymentMethodEditForm extends PaymentGatewayFormBase implements ContainerInjectionInterface {
+
+  /**
+   * The store storage.
+   *
+   * @var \Drupal\commerce_store\StoreStorageInterface
+   */
+  protected $storeStorage;
+
+  /**
+   * The logger.
+   *
+   * @var \Drupal\Core\Logger\LoggerChannelInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new PaymentMethodEditForm object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerChannelFactoryInterface $logger_factory) {
+    $this->storeStorage = $entity_type_manager->getStorage('commerce_store');
+    $this->logger = $logger_factory->get('commerce_payment');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('logger.factory')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
+    $payment_method = $this->entity;
+    $billing_profile = $payment_method->getBillingProfile();
+    $store = $this->storeStorage->loadDefault();
+
+    $form['#tree'] = TRUE;
+    $form['#attached']['library'][] = 'commerce_payment/payment_method_form';
+    $form['billing_information'] = [
+      '#parents' => array_merge($form['#parents'], ['billing_information']),
+      '#type' => 'commerce_profile_select',
+      '#default_value' => $billing_profile,
+      '#default_country' => $store ? $store->getAddress()->getCountryCode() : NULL,
+      '#available_countries' => $store ? $store->getBillingCountries() : [],
+      '#weight' => 50,
+    ];
+    if ($payment_method->bundle() == 'credit_card') {
+      $form['payment_details'] = $this->buildCreditCardForm($payment_method, $form_state);
+    }
+    elseif ($payment_method->bundle() == 'paypal') {
+      // @todo Decide how to handle saved PayPal payment methods.
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */
+    $payment_method = $this->entity;
+    $payment_method->setBillingProfile($form['billing_information']['#profile']);
+
+    if ($payment_method->bundle() == 'credit_card') {
+      $expiration_date = $form_state->getValue(['payment_method', 'payment_details', 'expiration']);
+      $payment_method->get('card_exp_month')->setValue($expiration_date['month']);
+      $payment_method->get('card_exp_year')->setValue($expiration_date['year']);
+      $expires = CreditCard::calculateExpirationTimestamp($expiration_date['month'], $expiration_date['year']);
+      $payment_method->setExpiresTime($expires);
+    }
+    elseif ($payment_method->bundle() == 'paypal') {
+      // @todo Decide how to handle saved PayPal payment methods.
+    }
+
+    /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsUpdatingStoredPaymentMethodsInterface $payment_gateway_plugin */
+    $payment_gateway_plugin = $this->plugin;
+    // The payment method form is customer facing. For security reasons
+    // the returned errors need to be more generic.
+    try {
+      $payment_gateway_plugin->updatePaymentMethod($payment_method);
+      $payment_method->save();
+    }
+    catch (DeclineException $e) {
+      $this->logger->warning($e->getMessage());
+      throw new DeclineException(t('We encountered an error processing your payment method. Please verify your details and try again.'));
+    }
+    catch (PaymentGatewayException $e) {
+      $this->logger->error($e->getMessage());
+      throw new PaymentGatewayException(t('We encountered an unexpected error processing your payment method. Please try again later.'));
+    }
+  }
+
+
+  /**
+   * Builds the credit card form.
+   *
+   * @param \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method
+   *   The payment method.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the complete form.
+   *
+   * @return array
+   *   The built credit card form.
+   */
+  protected function buildCreditCardForm(PaymentMethodInterface $payment_method, FormStateInterface $form_state) {
+    // Build a month select list that shows months with a leading zero.
+    $months = [];
+    for ($i = 1; $i < 13; $i++) {
+      $month = str_pad($i, 2, '0', STR_PAD_LEFT);
+      $months[$month] = $month;
+    }
+    // Build a year select list that uses a 4 digit key with a 2 digit value.
+    $current_year_4 = date('Y');
+    $current_year_2 = date('y');
+    $years = [];
+    for ($i = 0; $i < 10; $i++) {
+      $years[$current_year_4 + $i] = $current_year_2 + $i;
+    }
+
+    $element['#attached']['library'][] = 'commerce_payment/payment_method_icons';
+    $element['#attributes']['class'][] = 'credit-card-form';
+    $element['type'] = [
+      '#type' => 'hidden',
+      '#value' => $payment_method->get('card_type')->value,
+    ];
+    $element['number'] = [
+      '#type' => 'inline_template',
+      '#template' => '<span class="payment-method-icon payment-method-icon--{{ type }}"></span>Credit card ending in {{ mask }}',
+      '#context' => [
+        'type' => $payment_method->get('card_type')->value,
+        'mask' => $payment_method->get('card_number')->value
+      ],
+    ];
+    $element['expiration'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['credit-card-form__expiration'],
+      ],
+    ];
+    $element['expiration']['month'] = [
+      '#type' => 'select',
+      '#title' => t('Month'),
+      '#options' => $months,
+      '#default_value' => str_pad($payment_method->get('card_exp_month')->value, 2, '0', STR_PAD_LEFT),
+      '#required' => TRUE,
+    ];
+    $element['expiration']['divider'] = [
+      '#type' => 'item',
+      '#title' => '',
+      '#markup' => '<span class="credit-card-form__divider">/</span>',
+    ];
+    $element['expiration']['year'] = [
+      '#type' => 'select',
+      '#title' => t('Year'),
+      '#options' => $years,
+      '#default_value' => $payment_method->get('card_exp_year')->value,
+      '#required' => TRUE,
+    ];
+
+    return $element;
+  }
 
 }
diff --git a/modules/payment/tests/src/Functional/PaymentMethodTest.php b/modules/payment/tests/src/Functional/PaymentMethodTest.php
index c859ef33..ed559311 100644
--- a/modules/payment/tests/src/Functional/PaymentMethodTest.php
+++ b/modules/payment/tests/src/Functional/PaymentMethodTest.php
@@ -75,9 +75,9 @@ class PaymentMethodTest extends CommerceBrowserTestBase {
   }
 
   /**
-   * Tests creating a payment method.
+   * Tests creating and updating a payment method.
    */
-  public function testPaymentMethodCreation() {
+  public function testPaymentMethodCreationAndUpdate() {
     /** @var \Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway\OnsiteInterface $plugin */
     $this->drupalGet($this->collectionUrl);
     $this->getSession()->getPage()->clickLink('Add payment method');
@@ -100,7 +100,35 @@ class PaymentMethodTest extends CommerceBrowserTestBase {
     $this->assertSession()->pageTextContains('Visa ending in 1111 saved to your payment methods.');
 
     $payment_method = PaymentMethod::load(1);
+    $billing_profile = $payment_method->getBillingProfile();
     $this->assertEquals($this->user->id(), $payment_method->getOwnerId());
+    $this->assertEquals('NY', $billing_profile->get('address')->first()->getAdministrativeArea());
+    $this->assertEquals(1, $payment_method->getBillingProfile()->id());
+
+    $this->drupalGet($this->collectionUrl . '/' . $payment_method->id() . '/edit');
+    $form_values = [
+      'payment_method[payment_details][expiration][month]' => '02',
+      'payment_method[payment_details][expiration][year]' => '2026',
+      'payment_method[billing_information][address][0][address][given_name]' => 'Johnny',
+      'payment_method[billing_information][address][0][address][family_name]' => 'Appleseed',
+      'payment_method[billing_information][address][0][address][address_line1]' => '123 New York Drive',
+      'payment_method[billing_information][address][0][address][locality]' => 'Greenville',
+      'payment_method[billing_information][address][0][address][administrative_area]' => 'SC',
+      'payment_method[billing_information][address][0][address][postal_code]' => '29615',
+    ];
+    $this->submitForm($form_values, 'Save');
+    $this->assertSession()->addressEquals($this->collectionUrl);
+
+    $this->assertSession()->pageTextContains('2/2024');
+
+    \Drupal::entityTypeManager()->getStorage('commerce_payment_method')->resetCache([1]);
+    \Drupal::entityTypeManager()->getStorage('profile')->resetCache([1]);
+    $payment_method = PaymentMethod::load(1);
+    $this->assertEquals('2024', $payment_method->get('card_exp_year')->value);
+    $billing_profile = $payment_method->getBillingProfile();
+    $this->assertEquals($this->user->id(), $payment_method->getOwnerId());
+    $this->assertEquals('SC', $billing_profile->get('address')->first()->getAdministrativeArea());
+    $this->assertEquals(1, $payment_method->getBillingProfile()->id());
   }
 
   /**
diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
index 6b258dd7..b9eaaca2 100644
--- a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
+++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/Onsite.php
@@ -23,6 +23,7 @@ use Drupal\Core\Form\FormStateInterface;
  *   display_label = "Example",
  *   forms = {
  *     "add-payment-method" = "Drupal\commerce_payment_example\PluginForm\Onsite\PaymentMethodAddForm",
+ *     "edit-payment-method" = "Drupal\commerce_payment\PluginForm\PaymentMethodEditForm",
  *   },
  *   payment_method_types = {"credit_card"},
  *   credit_card_types = {
@@ -223,4 +224,15 @@ class Onsite extends OnsitePaymentGatewayBase implements OnsiteInterface {
     $payment_method->delete();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function updatePaymentMethod(PaymentMethodInterface $payment_method) {
+    // The default payment method edit form only supports updating billing info.
+    $billing_profile = $payment_method->getBillingProfile();
+
+    // Perform the update request here, throw an exception if it fails.
+    // See \Drupal\commerce_payment\Exception for the available exceptions.
+  }
+
 }
diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OnsiteInterface.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OnsiteInterface.php
index f9e115c8..e5f5e394 100644
--- a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OnsiteInterface.php
+++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OnsiteInterface.php
@@ -5,6 +5,7 @@ namespace Drupal\commerce_payment_example\Plugin\Commerce\PaymentGateway;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsAuthorizationsInterface;
 use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsRefundsInterface;
+use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsUpdatingStoredPaymentMethodsInterface;
 
 /**
  * Provides the interface for the example_onsite payment gateway.
@@ -14,6 +15,6 @@ use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsRefundsInterf
  * the gateway has. The gateway plugin is free to expose additional methods,
  * which would be defined below.
  */
-interface OnsiteInterface extends OnsitePaymentGatewayInterface, SupportsAuthorizationsInterface, SupportsRefundsInterface {
+interface OnsiteInterface extends OnsitePaymentGatewayInterface, SupportsAuthorizationsInterface, SupportsRefundsInterface, SupportsUpdatingStoredPaymentMethodsInterface {
 
 }
