diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp
index 87dbcb66d8..539091ed9c 100644
--- a/esphome/components/i2c/i2c_bus_arduino.cpp
+++ b/esphome/components/i2c/i2c_bus_arduino.cpp
@@ -32,6 +32,17 @@ void ArduinoI2CBus::dump_config() {
   ESP_LOGCONFIG(TAG, "  SDA Pin: GPIO%u", this->sda_pin_);
   ESP_LOGCONFIG(TAG, "  SCL Pin: GPIO%u", this->scl_pin_);
   ESP_LOGCONFIG(TAG, "  Frequency: %u Hz", this->frequency_);
+  switch (this->recovery_result_) {
+    case RECOVERY_COMPLETED:
+      ESP_LOGCONFIG(TAG, "  Recovery: bus successfully recovered");
+      break;
+    case RECOVERY_FAILED_SCL_LOW:
+      ESP_LOGCONFIG(TAG, "  Recovery: failed, SCL is held low on the bus");
+      break;
+    case RECOVERY_FAILED_SDA_LOW:
+      ESP_LOGCONFIG(TAG, "  Recovery: failed, SDA is held low on the bus");
+      break;
+  }
   if (this->scan_) {
     ESP_LOGI(TAG, "Scanning i2c bus for active devices...");
     uint8_t found = 0;
@@ -92,31 +103,110 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn
   return ERROR_UNKNOWN;
 }
 
+/// Perform I2C bus recovery, see:
+/// https://www.nxp.com/docs/en/user-guide/UM10204.pdf
+/// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
 void ArduinoI2CBus::recover_() {
-  // Perform I2C bus recovery, see
-  // https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf
-  // or see the linux kernel implementation, e.g.
-  // https://elixir.bootlin.com/linux/v5.14.6/source/drivers/i2c/i2c-core-base.c#L200
+  ESP_LOGI(TAG, "Performing I2C bus recovery");
 
-  // try to get about 100kHz toggle frequency
-  const auto half_period_usec = 1000000 / 100000 / 2;
-  const auto recover_scl_periods = 9;
-
-  // configure scl as output
-  pinMode(scl_pin_, OUTPUT);  // NOLINT
-
-  // set scl high
-  digitalWrite(scl_pin_, 1);  // NOLINT
-
-  // in total generate 9 falling-rising edges
-  for (auto i = 0; i < recover_scl_periods; i++) {
-    delayMicroseconds(half_period_usec);
-    digitalWrite(scl_pin_, 0);  // NOLINT
-    delayMicroseconds(half_period_usec);
-    digitalWrite(scl_pin_, 1);  // NOLINT
+  // Activate the pull up resistor on the SCL pin. This should make the
+  // signal on the line HIGH. If SCL is pulled low on the I2C bus however,
+  // then some device is interfering with the SCL line. In that case,
+  // the I2C bus cannot be recovered.
+  pinMode(scl_pin_, INPUT_PULLUP);     // NOLINT
+  if (digitalRead(scl_pin_) == LOW) {  // NOLINT
+    ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the I2C bus");
+    recovery_result_ = RECOVERY_FAILED_SCL_LOW;
+    return;
   }
 
+  // From the specification:
+  // "If the data line (SDA) is stuck LOW, send nine clock pulses. The
+  //  device that held the bus LOW should release it sometime within
+  //  those nine clocks."
+  // We don't really have to detect if SDA is stuck low. We'll simply send
+  // nine clock pulses here, just in case SDA is stuck.
+
+  // Use a 100kHz toggle frequency (i.e. the maximum frequency for I2C
+  // running in standard-mode). The resulting frequency will be lower,
+  // because of the additional function calls that are done, but that
+  // is no problem.
+  const auto half_period_usec = 1000000 / 100000 / 2;
+
+  // Make sure that switching to mode OUTPUT will make SCL low, just in
+  // case other code has setup the pin to output a HIGH signal.
+  digitalWrite(scl_pin_, LOW);  // NOLINT
+
+  // Activate the pull up resistor for SDA, so after the clock pulse cycle
+  // we can verify if SDA is pulled high. Also make sure that switching to
+  // mode OUTPUT will make SDA low.
+  pinMode(sda_pin_, INPUT_PULLUP);  // NOLINT
+  digitalWrite(sda_pin_, LOW);      // NOLINT
+
+  ESP_LOGI(TAG, "Sending 9 clock pulses to drain any stuck device output");
   delayMicroseconds(half_period_usec);
+  for (auto i = 0; i < 9; i++) {
+    // Release pull up resistor and switch to output to make the signal LOW.
+    pinMode(scl_pin_, INPUT);   // NOLINT
+    pinMode(scl_pin_, OUTPUT);  // NOLINT
+    delayMicroseconds(half_period_usec);
+
+    // Release output and activate pull up resistor to make the signal HIGH.
+    pinMode(scl_pin_, INPUT);         // NOLINT
+    pinMode(scl_pin_, INPUT_PULLUP);  // NOLINT
+    delayMicroseconds(half_period_usec);
+
+    // When SCL is kept LOW at this point, we might be looking at a device
+    // that applies clock stretching. Wait for the release of the SCL line,
+    // but not forever. There is no specification for the maximum allowed
+    // time. We'll stick to 500ms here.
+    auto wait = 20;
+    while (wait-- && digitalRead(scl_pin_) == LOW) {  // NOLINT
+      delay(25);
+    }
+    if (digitalRead(scl_pin_) == LOW) {  // NOLINT
+      ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle");
+      recovery_result_ = RECOVERY_FAILED_SCL_LOW;
+      return;
+    }
+  }
+
+  // By now, any stuck device ought to have sent all remaining bits of its
+  // transation, meaning that it should have freed up the SDA line, resulting
+  // in SDA being pulled up.
+  if (digitalRead(sda_pin_) == LOW) {  // NOLINT
+    ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle");
+    recovery_result_ = RECOVERY_FAILED_SDA_LOW;
+    return;
+  }
+
+  // From the specification:
+  // "I2C-bus compatible devices must reset their bus logic on receipt of
+  //  a START or repeated START condition such that they all anticipate
+  //  the sending of a target address, even if these START conditions are
+  //  not positioned according to the proper format."
+  // While the 9 clock pulses from above might have drained all bits of a
+  // single byte within a transaction, a device might have more bytes to
+  // transmit. So here we'll generate a START condition to snap the device
+  // out of this state.
+  // SCL and SDA are already high at this point, so we can generate a START
+  // condition by making the SDA signal LOW.
+  ESP_LOGI(TAG, "Generate START condition to reset bus logic of I2C devices");
+  pinMode(sda_pin_, INPUT);   // NOLINT
+  pinMode(sda_pin_, OUTPUT);  // NOLINT
+  delayMicroseconds(half_period_usec);
+
+  // From the specification:
+  // "A START condition immediately followed by a STOP condition (void
+  //  message) is an illegal format. Many devices however are designed to
+  //  operate properly under this condition."
+  // Finally, we'll bring the I2C bus into a starting state by generating
+  // a STOP condition.
+  ESP_LOGI(TAG, "Generate STOP condition to finalize recovery");
+  pinMode(sda_pin_, INPUT);         // NOLINT
+  pinMode(sda_pin_, INPUT_PULLUP);  // NOLINT
+
+  recovery_result_ = RECOVERY_COMPLETED;
 }
 }  // namespace i2c
 }  // namespace esphome
diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h
index 42589dcfb7..82f043ef7d 100644
--- a/esphome/components/i2c/i2c_bus_arduino.h
+++ b/esphome/components/i2c/i2c_bus_arduino.h
@@ -9,6 +9,12 @@
 namespace esphome {
 namespace i2c {
 
+enum RecoveryCode {
+  RECOVERY_FAILED_SCL_LOW,
+  RECOVERY_FAILED_SDA_LOW,
+  RECOVERY_COMPLETED,
+};
+
 class ArduinoI2CBus : public I2CBus, public Component {
  public:
   void setup() override;
@@ -24,6 +30,7 @@ class ArduinoI2CBus : public I2CBus, public Component {
 
  private:
   void recover_();
+  RecoveryCode recovery_result_;
 
  protected:
   TwoWire *wire_;